feat(db): migrate paginated endpoints to cursor-based pagination

This commit is contained in:
Philipinho
2026-01-23 19:01:54 +00:00
parent 9ec0f46eb6
commit 5739a0e260
21 changed files with 192 additions and 154 deletions
@@ -9,16 +9,14 @@ import { UpdateCommentDto } from './dto/update-comment.dto';
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo'; import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
import { Comment, Page, User } from '@docmost/db/types/entity.types'; import { Comment, Page, User } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { PaginationResult } from '@docmost/db/pagination/pagination';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class CommentService { export class CommentService {
constructor( constructor(
private commentRepo: CommentRepo, private commentRepo: CommentRepo,
private pageRepo: PageRepo, private pageRepo: PageRepo,
private spaceMemberRepo: SpaceMemberRepo,
) {} ) {}
async findById(commentId: string) { async findById(commentId: string) {
@@ -68,14 +66,14 @@ export class CommentService {
async findByPageId( async findByPageId(
pageId: string, pageId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Comment>> { ): Promise<CursorPaginationResult<Comment>> {
const page = await this.pageRepo.findById(pageId); const page = await this.pageRepo.findById(pageId);
if (!page) { if (!page) {
throw new BadRequestException('Page not found'); throw new BadRequestException('Page not found');
} }
return await this.commentRepo.findPageComments(pageId, pagination); return this.commentRepo.findPageComments(pageId, pagination);
} }
async update( async update(
@@ -11,7 +11,7 @@ import { UpdateGroupDto } from '../dto/update-group.dto';
import { KyselyTransaction } from '@docmost/db/types/kysely.types'; import { KyselyTransaction } from '@docmost/db/types/kysely.types';
import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { Group, InsertableGroup, User } from '@docmost/db/types/entity.types'; import { Group, InsertableGroup, User } from '@docmost/db/types/entity.types';
import { PaginationResult } from '@docmost/db/pagination/pagination'; import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
import { GroupUserService } from './group-user.service'; import { GroupUserService } from './group-user.service';
@Injectable() @Injectable()
@@ -132,12 +132,8 @@ export class GroupService {
async getWorkspaceGroups( async getWorkspaceGroups(
workspaceId: string, workspaceId: string,
paginationOptions: PaginationOptions, paginationOptions: PaginationOptions,
): Promise<PaginationResult<Group>> { ): Promise<CursorPaginationResult<Group>> {
const groups = await this.groupRepo.getGroupsPaginated( return this.groupRepo.getGroupsPaginated(workspaceId, paginationOptions);
workspaceId,
paginationOptions,
);
return groups;
} }
async deleteGroup(groupId: string, workspaceId: string): Promise<void> { async deleteGroup(groupId: string, workspaceId: string): Promise<void> {
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
import { PageHistory } from '@docmost/db/types/entity.types'; import { PageHistory } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { PaginationResult } from '@docmost/db/pagination/pagination'; import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class PageHistoryService { export class PageHistoryService {
@@ -15,12 +15,10 @@ export class PageHistoryService {
async findHistoryByPageId( async findHistoryByPageId(
pageId: string, pageId: string,
paginationOptions: PaginationOptions, paginationOptions: PaginationOptions,
): Promise<PaginationResult<any>> { ): Promise<CursorPaginationResult<PageHistory>> {
const pageHistory = await this.pageHistoryRepo.findPageHistoryByPageId( return this.pageHistoryRepo.findPageHistoryByPageId(
pageId, pageId,
paginationOptions, paginationOptions,
); );
return pageHistory;
} }
} }
@@ -10,9 +10,9 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types'; import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { import {
executeWithPagination, CursorPaginationResult,
PaginationResult, executeWithCursorPagination,
} from '@docmost/db/pagination/pagination'; } from '@docmost/db/pagination/cursor-pagination';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types'; import { KyselyDB } from '@docmost/db/types/kysely.types';
import { generateJitteredKeyBetween } from 'fractional-indexing-jittered'; import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
@@ -180,7 +180,7 @@ export class PageService {
spaceId: string, spaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
pageId?: string, pageId?: string,
): Promise<any> { ): Promise<CursorPaginationResult<Partial<Page> & { hasChildren: boolean }>> {
let query = this.db let query = this.db
.selectFrom('pages') .selectFrom('pages')
.select([ .select([
@@ -195,7 +195,6 @@ export class PageService {
'deletedAt', 'deletedAt',
]) ])
.select((eb) => this.pageRepo.withHasChildren(eb)) .select((eb) => this.pageRepo.withHasChildren(eb))
.orderBy('position', (ob) => ob.collate('C').asc())
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('spaceId', '=', spaceId); .where('spaceId', '=', spaceId);
@@ -205,12 +204,18 @@ export class PageService {
query = query.where('parentPageId', 'is', null); query = query.where('parentPageId', 'is', null);
} }
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: 250, perPage: 250,
after: pagination.cursor,
fields: [
{ expression: 'position', direction: 'asc', orderModifier: (ob) => ob.collate('C').asc() },
{ expression: 'id', direction: 'asc' },
],
parseCursor: (cursor) => ({
position: cursor.position,
id: cursor.id,
}),
}); });
return result;
} }
async movePageToSpace(rootPage: Page, spaceId: string) { async movePageToSpace(rootPage: Page, spaceId: string) {
@@ -259,7 +264,7 @@ export class PageService {
await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, { await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, {
pageId: pageIds, pageId: pageIds,
workspaceId: rootPage.workspaceId workspaceId: rootPage.workspaceId,
}); });
} }
}); });
@@ -387,9 +392,14 @@ export class PageService {
workspaceId: page.workspaceId, workspaceId: page.workspaceId,
creatorId: authUser.id, creatorId: authUser.id,
lastUpdatedById: authUser.id, lastUpdatedById: authUser.id,
parentPageId: page.id === rootPage.id parentPageId:
? (isDuplicateInSameSpace ? rootPage.parentPageId : null) page.id === rootPage.id
: (page.parentPageId ? pageMap.get(page.parentPageId)?.newPageId : null), ? isDuplicateInSameSpace
? rootPage.parentPageId
: null
: page.parentPageId
? pageMap.get(page.parentPageId)?.newPageId
: null,
}; };
}), }),
); );
@@ -569,22 +579,22 @@ export class PageService {
async getRecentSpacePages( async getRecentSpacePages(
spaceId: string, spaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Page>> { ): Promise<CursorPaginationResult<Page>> {
return await this.pageRepo.getRecentPagesInSpace(spaceId, pagination); return this.pageRepo.getRecentPagesInSpace(spaceId, pagination);
} }
async getRecentPages( async getRecentPages(
userId: string, userId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Page>> { ): Promise<CursorPaginationResult<Page>> {
return await this.pageRepo.getRecentPages(userId, pagination); return this.pageRepo.getRecentPages(userId, pagination);
} }
async getDeletedSpacePages( async getDeletedSpacePages(
spaceId: string, spaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Page>> { ): Promise<CursorPaginationResult<Page>> {
return await this.pageRepo.getDeletedPagesInSpace(spaceId, pagination); return this.pageRepo.getDeletedPagesInSpace(spaceId, pagination);
} }
async forceDelete(pageId: string, workspaceId: string): Promise<void> { async forceDelete(pageId: string, workspaceId: string): Promise<void> {
@@ -13,7 +13,7 @@ import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
import { RemoveSpaceMemberDto } from '../dto/remove-space-member.dto'; import { RemoveSpaceMemberDto } from '../dto/remove-space-member.dto';
import { UpdateSpaceMemberRoleDto } from '../dto/update-space-member-role.dto'; import { UpdateSpaceMemberRoleDto } from '../dto/update-space-member-role.dto';
import { SpaceRole } from '../../../common/helpers/types/permission'; import { SpaceRole } from '../../../common/helpers/types/permission';
import { PaginationResult } from '@docmost/db/pagination/pagination'; import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class SpaceMemberService { export class SpaceMemberService {
@@ -68,18 +68,16 @@ export class SpaceMemberService {
spaceId: string, spaceId: string,
workspaceId: string, workspaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
) { ): Promise<CursorPaginationResult<any>> {
const space = await this.spaceRepo.findById(spaceId, workspaceId); const space = await this.spaceRepo.findById(spaceId, workspaceId);
if (!space) { if (!space) {
throw new NotFoundException('Space not found'); throw new NotFoundException('Space not found');
} }
const members = await this.spaceMemberRepo.getSpaceMembersPaginated( return await this.spaceMemberRepo.getSpaceMembersPaginated(
spaceId, spaceId,
pagination, pagination,
); );
return members;
} }
async addMembersToSpaceBatch( async addMembersToSpaceBatch(
@@ -276,7 +274,7 @@ export class SpaceMemberService {
async getUserSpaces( async getUserSpaces(
userId: string, userId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Space>> { ): Promise<CursorPaginationResult<Space>> {
return await this.spaceMemberRepo.getUserSpaces(userId, pagination); return this.spaceMemberRepo.getUserSpaces(userId, pagination);
} }
} }
@@ -8,7 +8,6 @@ import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { Space, User } from '@docmost/db/types/entity.types'; import { Space, User } from '@docmost/db/types/entity.types';
import { PaginationResult } from '@docmost/db/pagination/pagination';
import { UpdateSpaceDto } from '../dto/update-space.dto'; import { UpdateSpaceDto } from '../dto/update-space.dto';
import { executeTx } from '@docmost/db/utils'; import { executeTx } from '@docmost/db/utils';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
@@ -17,6 +16,7 @@ import { SpaceRole } from '../../../common/helpers/types/permission';
import { QueueJob, QueueName } from 'src/integrations/queue/constants'; import { QueueJob, QueueName } from 'src/integrations/queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class SpaceService { export class SpaceService {
@@ -130,13 +130,8 @@ export class SpaceService {
async getWorkspaceSpaces( async getWorkspaceSpaces(
workspaceId: string, workspaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Space>> { ): Promise<CursorPaginationResult<Space>> {
const spaces = await this.spaceRepo.getSpacesInWorkspace( return this.spaceRepo.getSpacesInWorkspace(workspaceId, pagination);
workspaceId,
pagination,
);
return spaces;
} }
async deleteSpace(spaceId: string, workspaceId: string): Promise<void> { async deleteSpace(spaceId: string, workspaceId: string): Promise<void> {
@@ -23,7 +23,7 @@ import InvitationAcceptedEmail from '@docmost/transactional/emails/invitation-ac
import { TokenService } from '../../auth/services/token.service'; import { TokenService } from '../../auth/services/token.service';
import { nanoIdGen } from '../../../common/helpers'; import { nanoIdGen } from '../../../common/helpers';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { DomainService } from 'src/integrations/environment/domain.service'; import { DomainService } from 'src/integrations/environment/domain.service';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { QueueJob, QueueName } from '../../../integrations/queue/constants';
@@ -64,12 +64,12 @@ export class WorkspaceInvitationService {
); );
} }
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'asc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
return result;
} }
async getInvitationById(invitationId: string, workspace: Workspace) { async getInvitationById(invitationId: string, workspace: Workspace) {
@@ -19,7 +19,6 @@ import { User } from '@docmost/db/types/entity.types';
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo'; import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { PaginationResult } from '@docmost/db/pagination/pagination';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto'; import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { UserRepo } from '@docmost/db/repos/user/user.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
@@ -28,12 +27,12 @@ import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants'; import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { AttachmentType } from 'src/core/attachment/attachment.constants';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { generateRandomSuffixNumbers } from '../../../common/helpers'; import { generateRandomSuffixNumbers } from '../../../common/helpers';
import { isPageEmbeddingsTableExists } from '@docmost/db/helpers/helpers'; import { isPageEmbeddingsTableExists } from '@docmost/db/helpers/helpers';
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
@@ -376,13 +375,8 @@ export class WorkspaceService {
async getWorkspaceUsers( async getWorkspaceUsers(
workspaceId: string, workspaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<User>> { ): Promise<CursorPaginationResult<User>> {
const users = await this.userRepo.getUsersPaginated( return this.userRepo.getUsersPaginated(workspaceId, pagination);
workspaceId,
pagination,
);
return users;
} }
async updateWorkspaceUserRole( async updateWorkspaceUserRole(
@@ -110,7 +110,7 @@ type CursorPaginationResultRow<
type CursorPaginationMeta = { type CursorPaginationMeta = {
limit: number; limit: number;
hasMore: boolean; hasNextPage: boolean;
nextCursor: string | null; nextCursor: string | null;
}; };
@@ -233,16 +233,16 @@ export async function executeWithCursorPagination<
const rows = await qb.limit(opts.perPage + 1).execute(); const rows = await qb.limit(opts.perPage + 1).execute();
const hasMore = rows.length > opts.perPage; const hasNextPage = rows.length > opts.perPage;
// If we fetched an extra row to determine if we have a next page, that // If we fetched an extra row to determine if we have a next page, that
// shouldn't be in the returned results // shouldn't be in the returned results
if (hasMore) rows.pop(); if (hasNextPage) rows.pop();
if (reversed) rows.reverse(); if (reversed) rows.reverse();
const endRow = rows[rows.length - 1]; const endRow = rows[rows.length - 1];
const nextCursor = hasMore && endRow ? generateCursor(endRow) : null; const nextCursor = hasNextPage && endRow ? generateCursor(endRow) : null;
return { return {
items: rows.map((row) => { items: rows.map((row) => {
@@ -257,7 +257,7 @@ export async function executeWithCursorPagination<
}), }),
meta: { meta: {
limit: opts.perPage, limit: opts.perPage,
hasMore, hasNextPage,
nextCursor, nextCursor,
}, },
}; };
@@ -21,6 +21,10 @@ export class PaginationOptions {
@Max(100) @Max(100)
limit = 20; limit = 20;
@IsOptional()
@IsString()
cursor?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
query: string; query: string;
@@ -8,7 +8,7 @@ import {
UpdatableComment, UpdatableComment,
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { ExpressionBuilder } from 'kysely'; import { ExpressionBuilder } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { jsonObjectFrom } from 'kysely/helpers/postgres';
@@ -37,15 +37,14 @@ export class CommentRepo {
.selectAll('comments') .selectAll('comments')
.select((eb) => this.withCreator(eb)) .select((eb) => this.withCreator(eb))
.select((eb) => this.withResolvedBy(eb)) .select((eb) => this.withResolvedBy(eb))
.where('pageId', '=', pageId) .where('pageId', '=', pageId);
.orderBy('createdAt', 'asc');
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'asc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
return result;
} }
async updateComment( async updateComment(
@@ -9,7 +9,7 @@ import { dbOrTx, executeTx } from '@docmost/db/utils';
import { sql } from 'kysely'; import { sql } from 'kysely';
import { GroupUser, InsertableGroupUser } from '@docmost/db/types/entity.types'; import { GroupUser, InsertableGroupUser } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options'; import { PaginationOptions } from '../../pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { UserRepo } from '@docmost/db/repos/user/user.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo';
@@ -52,8 +52,7 @@ export class GroupUserRepo {
.selectFrom('groupUsers') .selectFrom('groupUsers')
.innerJoin('users', 'users.id', 'groupUsers.userId') .innerJoin('users', 'users.id', 'groupUsers.userId')
.selectAll('users') .selectAll('users')
.where('groupId', '=', groupId) .where('groupId', '=', groupId);
.orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => query = query.where((eb) =>
@@ -61,9 +60,11 @@ export class GroupUserRepo {
); );
} }
const result = await executeWithPagination(query, { const result = await executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'users.id', direction: 'asc', key: 'id' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
result.items.map((user) => { result.items.map((user) => {
@@ -10,8 +10,8 @@ import {
import { ExpressionBuilder, sql } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { PaginationOptions } from '../../pagination/pagination-options'; import { PaginationOptions } from '../../pagination/pagination-options';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { DefaultGroup } from '../../../core/group/dto/create-group.dto'; import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
@Injectable() @Injectable()
export class GroupRepo { export class GroupRepo {
@@ -104,17 +104,19 @@ export class GroupRepo {
} }
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) { async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
let query = this.db let baseQuery = this.db
.selectFrom('groups') .selectFrom('groups')
.selectAll('groups') .selectAll('groups')
.select((eb) => this.withMemberCount(eb)) .select((eb) => this.withMemberCount(eb))
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId);
.orderBy('memberCount', 'desc')
.orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => baseQuery = baseQuery.where((eb) =>
eb(sql`f_unaccent(name)`, 'ilike', sql`f_unaccent(${'%' + pagination.query + '%'})`).or( eb(
sql`f_unaccent(name)`,
'ilike',
sql`f_unaccent(${'%' + pagination.query + '%'})`,
).or(
sql`f_unaccent(description)`, sql`f_unaccent(description)`,
'ilike', 'ilike',
sql`f_unaccent(${'%' + pagination.query + '%'})`, sql`f_unaccent(${'%' + pagination.query + '%'})`,
@@ -122,12 +124,23 @@ export class GroupRepo {
); );
} }
const result = executeWithPagination(query, { const query = this.db.selectFrom(baseQuery.as('sub')).selectAll('sub');
page: pagination.page, return executeWithCursorPagination(query, {
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{
expression: 'sub.memberCount',
direction: 'desc',
key: 'memberCount',
},
{ expression: 'sub.id', direction: 'asc', key: 'id' },
],
parseCursor: (cursor) => ({
memberCount: parseInt(cursor.memberCount, 10),
id: cursor.id,
}),
}); });
return result;
} }
withMemberCount(eb: ExpressionBuilder<DB, 'groups'>) { withMemberCount(eb: ExpressionBuilder<DB, 'groups'>) {
@@ -8,7 +8,7 @@ import {
PageHistory, PageHistory,
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { ExpressionBuilder } from 'kysely'; import { ExpressionBuilder } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
@@ -65,15 +65,14 @@ export class PageHistoryRepo {
.selectFrom('pageHistory') .selectFrom('pageHistory')
.selectAll() .selectAll()
.select((eb) => this.withLastUpdatedBy(eb)) .select((eb) => this.withLastUpdatedBy(eb))
.where('pageId', '=', pageId) .where('pageId', '=', pageId);
.orderBy('createdAt', 'desc');
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'desc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
return result;
} }
async findPageLastHistory(pageId: string, trx?: KyselyTransaction) { async findPageLastHistory(pageId: string, trx?: KyselyTransaction) {
@@ -8,7 +8,7 @@ import {
UpdatablePage, UpdatablePage,
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
import { ExpressionBuilder, sql } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
@@ -281,15 +281,20 @@ export class PageRepo {
.select(this.baseFields) .select(this.baseFields)
.select((eb) => this.withSpace(eb)) .select((eb) => this.withSpace(eb))
.where('spaceId', '=', spaceId) .where('spaceId', '=', spaceId)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null);
.orderBy('updatedAt', 'desc');
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{ expression: 'updatedAt', direction: 'desc' },
{ expression: 'id', direction: 'desc' },
],
parseCursor: (cursor) => ({
updatedAt: new Date(cursor.updatedAt),
id: cursor.id,
}),
}); });
return result;
} }
async getRecentPages(userId: string, pagination: PaginationOptions) { async getRecentPages(userId: string, pagination: PaginationOptions) {
@@ -298,12 +303,19 @@ export class PageRepo {
.select(this.baseFields) .select(this.baseFields)
.select((eb) => this.withSpace(eb)) .select((eb) => this.withSpace(eb))
.where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId))
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null);
.orderBy('updatedAt', 'desc');
return executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{ expression: 'updatedAt', direction: 'desc' },
{ expression: 'id', direction: 'desc' },
],
parseCursor: (cursor) => ({
updatedAt: new Date(cursor.updatedAt),
id: cursor.id,
}),
}); });
} }
@@ -331,15 +343,20 @@ export class PageRepo {
), ),
), ),
]), ]),
) );
.orderBy('deletedAt', 'desc');
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{ expression: 'deletedAt', direction: 'desc' },
{ expression: 'id', direction: 'desc' },
],
parseCursor: (cursor) => ({
deletedAt: new Date(cursor.deletedAt),
id: cursor.id,
}),
}); });
return result;
} }
withSpace(eb: ExpressionBuilder<DB, 'pages'>) { withSpace(eb: ExpressionBuilder<DB, 'pages'>) {
@@ -8,7 +8,7 @@ import {
UpdatableShare, UpdatableShare,
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
import { ExpressionBuilder, sql } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
@@ -143,12 +143,19 @@ export class ShareRepo {
.select((eb) => this.withPage(eb)) .select((eb) => this.withPage(eb))
.select((eb) => this.withSpace(eb, userId)) .select((eb) => this.withSpace(eb, userId))
.select((eb) => this.withCreator(eb)) .select((eb) => this.withCreator(eb))
.where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId));
.orderBy('updatedAt', 'desc');
return executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{ expression: 'updatedAt', direction: 'desc' },
{ expression: 'id', direction: 'desc' },
],
parseCursor: (cursor) => ({
updatedAt: new Date(cursor.updatedAt),
id: cursor.id,
}),
}); });
} }
@@ -10,7 +10,7 @@ import {
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options'; import { PaginationOptions } from '../../pagination/pagination-options';
import { MemberInfo, UserSpaceRole } from './types'; import { MemberInfo, UserSpaceRole } from './types';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
@@ -98,7 +98,7 @@ export class SpaceMemberRepo {
spaceId: string, spaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
) { ) {
let query = this.db let baseQuery = this.db
.selectFrom('spaceMembers') .selectFrom('spaceMembers')
.leftJoin('users', 'users.id', 'spaceMembers.userId') .leftJoin('users', 'users.id', 'spaceMembers.userId')
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId') .leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
@@ -114,12 +114,11 @@ export class SpaceMemberRepo {
'spaceMembers.createdAt', 'spaceMembers.createdAt',
]) ])
.select((eb) => this.groupRepo.withMemberCount(eb)) .select((eb) => this.groupRepo.withMemberCount(eb))
.where('spaceId', '=', spaceId) .select(sql<number>`case when groups.id is not null then 1 else 0 end`.as('isGroup'))
.orderBy((eb) => eb('groups.id', 'is not', null), 'desc') .where('spaceId', '=', spaceId);
.orderBy('spaceMembers.createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => baseQuery = baseQuery.where((eb) =>
eb( eb(
sql`f_unaccent(users.name)`, sql`f_unaccent(users.name)`,
'ilike', 'ilike',
@@ -138,9 +137,19 @@ export class SpaceMemberRepo {
); );
} }
const result = await executeWithPagination(query, { const query = this.db.selectFrom(baseQuery.as('sub')).selectAll('sub');
page: pagination.page,
const result = await executeWithCursorPagination(query, {
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [
{ expression: 'sub.isGroup', direction: 'desc', key: 'isGroup' },
{ expression: 'sub.createdAt', direction: 'asc', key: 'createdAt' },
],
parseCursor: (cursor) => ({
isGroup: parseInt(cursor.isGroup, 10),
createdAt: new Date(cursor.createdAt),
}),
}); });
let memberInfo: MemberInfo; let memberInfo: MemberInfo;
@@ -235,8 +244,7 @@ export class SpaceMemberRepo {
.selectFrom('spaces') .selectFrom('spaces')
.selectAll() .selectAll()
.select((eb) => [this.spaceRepo.withMemberCount(eb)]) .select((eb) => [this.spaceRepo.withMemberCount(eb)])
.where('id', 'in', this.getUserSpaceIdsQuery(userId)) .where('id', 'in', this.getUserSpaceIdsQuery(userId));
.orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => query = query.where((eb) =>
@@ -252,9 +260,11 @@ export class SpaceMemberRepo {
); );
} }
return executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'asc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
} }
} }
@@ -9,7 +9,7 @@ import {
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { ExpressionBuilder, sql } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { PaginationOptions } from '../../pagination/pagination-options'; import { PaginationOptions } from '../../pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -110,8 +110,7 @@ export class SpaceRepo {
.selectFrom('spaces') .selectFrom('spaces')
.selectAll('spaces') .selectAll('spaces')
.select((eb) => [this.withMemberCount(eb)]) .select((eb) => [this.withMemberCount(eb)])
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId);
.orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => query = query.where((eb) =>
@@ -127,12 +126,12 @@ export class SpaceRepo {
); );
} }
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'asc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
return result;
} }
withMemberCount(eb: ExpressionBuilder<DB, 'spaces'>) { withMemberCount(eb: ExpressionBuilder<DB, 'spaces'>) {
@@ -10,7 +10,7 @@ import {
User, User,
} from '@docmost/db/types/entity.types'; } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options'; import { PaginationOptions } from '../../pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { ExpressionBuilder, sql } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { jsonObjectFrom } from 'kysely/helpers/postgres';
@@ -145,8 +145,7 @@ export class UserRepo {
.selectFrom('users') .selectFrom('users')
.select(this.baseFields) .select(this.baseFields)
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null);
.orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
query = query.where((eb) => query = query.where((eb) =>
@@ -162,12 +161,12 @@ export class UserRepo {
); );
} }
const result = executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'asc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
return result;
} }
async updatePreference( async updatePreference(
@@ -27,7 +27,7 @@ import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { FileTaskIdDto } from './dto/file-task-dto'; import { FileTaskIdDto } from './dto/file-task-dto';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
@Controller('file-tasks') @Controller('file-tasks')
export class FileTaskController { export class FileTaskController {
@@ -56,12 +56,13 @@ export class FileTaskController {
const query = this.db const query = this.db
.selectFrom('fileTasks') .selectFrom('fileTasks')
.selectAll() .selectAll()
.where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(user.id)) .where('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(user.id));
.orderBy('createdAt', 'desc');
return executeWithPagination(query, { return executeWithCursorPagination(query, {
page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
after: pagination.cursor,
fields: [{ expression: 'id', direction: 'desc' }],
parseCursor: (cursor) => ({ id: cursor.id }),
}); });
} }