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 = [ '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 { 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 { const db = dbOrTx(this.db, trx); return db .insertInto('pages') .values(insertablePage) .returning(this.baseFields) .executeTakeFirst(); } async deletePage(pageId: string): Promise { 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) { return jsonObjectFrom( eb .selectFrom('spaces') .select(['spaces.id', 'spaces.name', 'spaces.slug']) .whereRef('spaces.id', '=', 'pages.spaceId'), ).as('space'); } withCreator(eb: ExpressionBuilder) { return jsonObjectFrom( eb .selectFrom('users') .select(['users.id', 'users.name', 'users.avatarUrl']) .whereRef('users.id', '=', 'pages.creatorId'), ).as('creator'); } withLastUpdatedBy(eb: ExpressionBuilder) { return jsonObjectFrom( eb .selectFrom('users') .select(['users.id', 'users.name', 'users.avatarUrl']) .whereRef('users.id', '=', 'pages.lastUpdatedById'), ).as('lastUpdatedBy'); } withContributors(eb: ExpressionBuilder) { 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(); } }