Files
docmost/apps/server/src/database/repos/page/page.repo.ts

251 lines
6.5 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
import { dbOrTx } from '../../utils';
import {
InsertablePage,
Page,
UpdatablePage,
} from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { validate as isValidUUID } from 'uuid';
import { ExpressionBuilder, sql } from 'kysely';
import { DB } from '@docmost/db/types/db';
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
@Injectable()
export class PageRepo {
constructor(
@InjectKysely() private readonly db: KyselyDB,
private spaceMemberRepo: SpaceMemberRepo,
) {}
private baseFields: Array<keyof Page> = [
'id',
'slugId',
'title',
'icon',
'coverPhoto',
'position',
'parentPageId',
'creatorId',
'lastUpdatedById',
'spaceId',
'workspaceId',
'isLocked',
'createdAt',
'updatedAt',
'deletedAt',
'contributorIds',
];
async findById(
pageId: string,
opts?: {
includeContent?: boolean;
includeYdoc?: boolean;
includeSpace?: boolean;
includeCreator?: boolean;
includeLastUpdatedBy?: boolean;
includeContributors?: boolean;
withLock?: boolean;
trx?: KyselyTransaction;
},
): Promise<Page> {
const db = dbOrTx(this.db, opts?.trx);
let query = db
.selectFrom('pages')
.select(this.baseFields)
.$if(opts?.includeContent, (qb) => qb.select('content'))
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'));
if (opts?.includeCreator) {
query = query.select((eb) => this.withCreator(eb));
}
if (opts?.includeLastUpdatedBy) {
query = query.select((eb) => this.withLastUpdatedBy(eb));
}
if (opts?.includeContributors) {
query = query.select((eb) => this.withContributors(eb));
}
if (opts?.includeSpace) {
query = query.select((eb) => this.withSpace(eb));
}
if (opts?.withLock && opts?.trx) {
query = query.forUpdate();
}
if (isValidUUID(pageId)) {
query = query.where('id', '=', pageId);
} else {
query = query.where('slugId', '=', pageId);
}
return query.executeTakeFirst();
}
async updatePage(
updatablePage: UpdatablePage,
pageId: string,
trx?: KyselyTransaction,
) {
return this.updatePages(updatablePage, [pageId], trx);
}
async updatePages(
updatePageData: UpdatablePage,
pageIds: string[],
trx?: KyselyTransaction,
) {
return dbOrTx(this.db, trx)
.updateTable('pages')
.set({ ...updatePageData, updatedAt: new Date() })
.where(
pageIds.some((pageId) => !isValidUUID(pageId)) ? 'slugId' : 'id',
'in',
pageIds,
)
.executeTakeFirst();
}
async insertPage(
insertablePage: InsertablePage,
trx?: KyselyTransaction,
): Promise<Page> {
const db = dbOrTx(this.db, trx);
return db
.insertInto('pages')
.values(insertablePage)
.returning(this.baseFields)
.executeTakeFirst();
}
async deletePage(pageId: string): Promise<void> {
let query = this.db.deleteFrom('pages');
if (isValidUUID(pageId)) {
query = query.where('id', '=', pageId);
} else {
query = query.where('slugId', '=', pageId);
}
await query.execute();
}
async getRecentPagesInSpace(spaceId: string, pagination: PaginationOptions) {
const query = this.db
.selectFrom('pages')
.select(this.baseFields)
.select((eb) => this.withSpace(eb))
.where('spaceId', '=', spaceId)
.orderBy('updatedAt', 'desc');
const result = executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
});
return result;
}
async getRecentPages(userId: string, pagination: PaginationOptions) {
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
const query = this.db
.selectFrom('pages')
.select(this.baseFields)
.select((eb) => this.withSpace(eb))
.where('spaceId', 'in', userSpaceIds)
.orderBy('updatedAt', 'desc');
const hasEmptyIds = userSpaceIds.length === 0;
const result = executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
hasEmptyIds,
});
return result;
}
withSpace(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonObjectFrom(
eb
.selectFrom('spaces')
.select(['spaces.id', 'spaces.name', 'spaces.slug'])
.whereRef('spaces.id', '=', 'pages.spaceId'),
).as('space');
}
withCreator(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'pages.creatorId'),
).as('creator');
}
withLastUpdatedBy(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'pages.lastUpdatedById'),
).as('lastUpdatedBy');
}
withContributors(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonArrayFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', sql`ANY(${eb.ref('pages.contributorIds')})`),
).as('contributors');
}
async getPageAndDescendants(parentPageId: string) {
return this.db
.withRecursive('page_hierarchy', (db) =>
db
.selectFrom('pages')
.select([
'id',
'slugId',
'title',
'icon',
'content',
'parentPageId',
'spaceId',
'workspaceId',
])
.where('id', '=', parentPageId)
.unionAll((exp) =>
exp
.selectFrom('pages as p')
.select([
'p.id',
'p.slugId',
'p.title',
'p.icon',
'p.content',
'p.parentPageId',
'p.spaceId',
'p.workspaceId',
])
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id'),
),
)
.selectFrom('page_hierarchy')
.selectAll()
.execute();
}
}