From 4913975e99a8f1bb9514e850301df69cac3e55f9 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 1 Apr 2024 01:23:52 +0100 Subject: [PATCH] server: refactor pagination * fix transaction usgae in repos * other bug fixes --- .../src/core/auth/services/signup.service.ts | 28 +--- .../src/core/comment/comment.controller.ts | 2 +- .../src/core/comment/comment.service.ts | 16 +- .../server/src/core/group/group.controller.ts | 2 +- .../core/group/services/group-user.service.ts | 44 +++-- .../src/core/group/services/group.service.ts | 32 ++-- .../src/core/page/dto/create-page.dto.ts | 4 +- .../src/core/page/dto/delete-page.dto.ts | 6 - .../src/core/page/dto/history-details.dto.ts | 6 - .../src/core/page/dto/page-details.dto.ts | 6 - .../src/core/page/dto/page-history.dto.ts | 6 - apps/server/src/core/page/dto/page.dto.ts | 16 ++ apps/server/src/core/page/page.controller.ts | 25 ++- .../page/services/page-history.service.ts | 19 +-- .../src/core/page/services/page.service.ts | 54 +++---- .../space/services/space-member.service.ts | 51 +++--- .../src/core/space/services/space.service.ts | 18 +-- .../server/src/core/space/space.controller.ts | 2 +- .../controllers/workspace.controller.ts | 2 +- .../services/workspace-user.service.ts | 16 +- .../helpers/pagination/paginated-result.ts | 14 -- .../helpers/pagination/pagination-meta-dto.ts | 29 ---- apps/server/src/kysely/kysely-db.module.ts | 5 +- .../pagination/pagination-options.ts | 21 ++- .../src/kysely/pagination/pagination.ts | 66 ++++++++ .../repos/attachment/attachment.repo.ts | 20 +-- .../src/kysely/repos/comment/comment.repo.ts | 69 ++++---- .../src/kysely/repos/group/group-user.repo.ts | 85 ++++------ .../src/kysely/repos/group/group.repo.ts | 107 +++++++------ .../kysely/repos/page/page-history.repo.ts | 104 +++++------- .../kysely/repos/page/page-ordering.repo.ts | 60 +------ .../server/src/kysely/repos/page/page.repo.ts | 74 ++++----- .../kysely/repos/space/space-member.repo.ts | 151 ++++++++---------- .../src/kysely/repos/space/space.repo.ts | 120 ++++++++------ .../server/src/kysely/repos/user/user.repo.ts | 62 ++++--- .../kysely/repos/workspace/workspace.repo.ts | 36 ++--- apps/server/src/kysely/types/entity.types.ts | 2 + apps/server/src/kysely/utils.ts | 24 ++- 38 files changed, 648 insertions(+), 756 deletions(-) delete mode 100644 apps/server/src/core/page/dto/delete-page.dto.ts delete mode 100644 apps/server/src/core/page/dto/history-details.dto.ts delete mode 100644 apps/server/src/core/page/dto/page-details.dto.ts delete mode 100644 apps/server/src/core/page/dto/page-history.dto.ts create mode 100644 apps/server/src/core/page/dto/page.dto.ts delete mode 100644 apps/server/src/helpers/pagination/paginated-result.ts delete mode 100644 apps/server/src/helpers/pagination/pagination-meta-dto.ts rename apps/server/src/{helpers => kysely}/pagination/pagination-options.ts (52%) create mode 100644 apps/server/src/kysely/pagination/pagination.ts diff --git a/apps/server/src/core/auth/services/signup.service.ts b/apps/server/src/core/auth/services/signup.service.ts index efecab4..1bb53ff 100644 --- a/apps/server/src/core/auth/services/signup.service.ts +++ b/apps/server/src/core/auth/services/signup.service.ts @@ -67,24 +67,6 @@ export class SignupService { ); } - async createWorkspace( - user: User, - workspaceName: string, - trx?: KyselyTransaction, - ) { - return await executeTx( - this.db, - async (trx) => { - const workspaceData: CreateWorkspaceDto = { - name: workspaceName, - }; - - return await this.workspaceService.create(user, workspaceData, trx); - }, - trx, - ); - } - async initialSetup( createAdminUserDto: CreateAdminUserDto, trx?: KyselyTransaction, @@ -94,9 +76,15 @@ export class SignupService { async (trx) => { // create user const user = await this.userRepo.insertUser(createAdminUserDto, trx); - const workspace = await this.createWorkspace( + + // create workspace with full setup + const workspaceData: CreateWorkspaceDto = { + name: createAdminUserDto.workspaceName, + }; + + const workspace = await this.workspaceService.create( user, - createAdminUserDto.workspaceName, + workspaceData, trx, ); diff --git a/apps/server/src/core/comment/comment.controller.ts b/apps/server/src/core/comment/comment.controller.ts index 367d7fa..c7611a4 100644 --- a/apps/server/src/core/comment/comment.controller.ts +++ b/apps/server/src/core/comment/comment.controller.ts @@ -13,7 +13,7 @@ import { CommentsInput, SingleCommentInput } from './dto/comments.input'; import { AuthUser } from '../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { User, Workspace } from '@docmost/db/types/entity.types'; @UseGuards(JwtAuthGuard) diff --git a/apps/server/src/core/comment/comment.service.ts b/apps/server/src/core/comment/comment.service.ts index d74714c..2991ed5 100644 --- a/apps/server/src/core/comment/comment.service.ts +++ b/apps/server/src/core/comment/comment.service.ts @@ -4,9 +4,8 @@ import { UpdateCommentDto } from './dto/update-comment.dto'; import { PageService } from '../page/services/page.service'; import { CommentRepo } from '@docmost/db/repos/comment/comment.repo'; import { Comment } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; -import { PaginatedResult } from 'src/helpers/pagination/paginated-result'; -import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class CommentService { @@ -63,15 +62,14 @@ export class CommentService { async findByPageId( pageId: string, - paginationOptions: PaginationOptions, - ): Promise> { - const { comments, count } = await this.commentRepo.findPageComments( + pagination: PaginationOptions, + ): Promise> { + const pageComments = await this.commentRepo.findPageComments( pageId, - paginationOptions, + pagination, ); - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(comments, paginationMeta); + return pageComments; } async update( diff --git a/apps/server/src/core/group/group.controller.ts b/apps/server/src/core/group/group.controller.ts index d19b23e..fee1e9e 100644 --- a/apps/server/src/core/group/group.controller.ts +++ b/apps/server/src/core/group/group.controller.ts @@ -12,7 +12,7 @@ import { AuthUser } from '../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; import { GroupUserService } from './services/group-user.service'; import { GroupIdDto } from './dto/group-id.dto'; -import { PaginationOptions } from '../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../kysely/pagination/pagination-options'; import { AddGroupUserDto } from './dto/add-group-user.dto'; import { RemoveGroupUserDto } from './dto/remove-group-user.dto'; import { UpdateGroupDto } from './dto/update-group.dto'; diff --git a/apps/server/src/core/group/services/group-user.service.ts b/apps/server/src/core/group/services/group-user.service.ts index fb5e8b3..dbcf333 100644 --- a/apps/server/src/core/group/services/group-user.service.ts +++ b/apps/server/src/core/group/services/group-user.service.ts @@ -1,14 +1,16 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; -import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import { GroupService } from './group.service'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { executeTx } from '@docmost/db/utils'; import { InjectKysely } from 'nestjs-kysely'; import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo'; -import { User } from '@docmost/db/types/entity.types'; +import { UserRepo } from '@docmost/db/repos/user/user.repo'; @Injectable() export class GroupUserService { @@ -16,24 +18,23 @@ export class GroupUserService { private groupRepo: GroupRepo, private groupUserRepo: GroupUserRepo, private groupService: GroupService, + private userRepo: UserRepo, @InjectKysely() private readonly db: KyselyDB, ) {} async getGroupUsers( groupId: string, workspaceId: string, - paginationOptions: PaginationOptions, - ): Promise> { + pagination: PaginationOptions, + ) { await this.groupService.findAndValidateGroup(groupId, workspaceId); - const { users, count } = await this.groupUserRepo.getGroupUsersPaginated( + const groupUsers = await this.groupUserRepo.getGroupUsersPaginated( groupId, - paginationOptions, + pagination, ); - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - - return new PaginatedResult(users, paginationMeta); + return groupUsers; } async addUserToDefaultGroup( @@ -63,7 +64,18 @@ export class GroupUserService { await executeTx( this.db, async (trx) => { - // await this.groupService.findAndValidateGroup(groupId, workspaceId); + await this.groupService.findAndValidateGroup(groupId, workspaceId); + const user = await this.userRepo.findById( + userId, + workspaceId, + false, + trx, + ); + + if (!user) { + throw new NotFoundException('User not found'); + } + const groupUserExists = await this.groupUserRepo.getGroupUserById( userId, groupId, @@ -98,6 +110,12 @@ export class GroupUserService { workspaceId, ); + const user = await this.userRepo.findById(userId, workspaceId); + + if (!user) { + throw new NotFoundException('User not found'); + } + if (group.isDefault) { throw new BadRequestException( 'You cannot remove users from a default group', diff --git a/apps/server/src/core/group/services/group.service.ts b/apps/server/src/core/group/services/group.service.ts index bf19ab0..2d2501f 100644 --- a/apps/server/src/core/group/services/group.service.ts +++ b/apps/server/src/core/group/services/group.service.ts @@ -4,13 +4,12 @@ import { NotFoundException, } from '@nestjs/common'; import { CreateGroupDto, DefaultGroup } from '../dto/create-group.dto'; -import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import { UpdateGroupDto } from '../dto/update-group.dto'; import { KyselyTransaction } from '@docmost/db/types/kysely.types'; import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { Group, InsertableGroup, User } from '@docmost/db/types/entity.types'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class GroupService { @@ -71,15 +70,16 @@ export class GroupService { throw new BadRequestException('You cannot update a default group'); } - const groupExists = await this.groupRepo.findByName( - updateGroupDto.name, - workspaceId, - ); - if (groupExists) { - throw new BadRequestException('Group name already exists'); - } - if (updateGroupDto.name) { + const existingGroup = await this.groupRepo.findByName( + updateGroupDto.name, + workspaceId, + ); + + if (existingGroup && group.name !== existingGroup.name) { + throw new BadRequestException('Group name already exists'); + } + group.name = updateGroupDto.name; } @@ -100,7 +100,6 @@ export class GroupService { } async getGroupInfo(groupId: string, workspaceId: string): Promise { - // todo: add member count const group = await this.groupRepo.findById(groupId, workspaceId); if (!group) { @@ -113,15 +112,12 @@ export class GroupService { async getWorkspaceGroups( workspaceId: string, paginationOptions: PaginationOptions, - ): Promise> { - const { groups, count } = await this.groupRepo.getGroupsPaginated( + ): Promise> { + const groups = await this.groupRepo.getGroupsPaginated( workspaceId, paginationOptions, ); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - - return new PaginatedResult(groups, paginationMeta); + return groups; } async deleteGroup(groupId: string, workspaceId: string): Promise { diff --git a/apps/server/src/core/page/dto/create-page.dto.ts b/apps/server/src/core/page/dto/create-page.dto.ts index e3383fb..f199c3a 100644 --- a/apps/server/src/core/page/dto/create-page.dto.ts +++ b/apps/server/src/core/page/dto/create-page.dto.ts @@ -14,9 +14,9 @@ export class CreatePageDto { icon?: string; @IsOptional() - @IsString() + @IsUUID() parentPageId?: string; - @IsString() + @IsUUID() spaceId: string; } diff --git a/apps/server/src/core/page/dto/delete-page.dto.ts b/apps/server/src/core/page/dto/delete-page.dto.ts deleted file mode 100644 index 9b091f8..0000000 --- a/apps/server/src/core/page/dto/delete-page.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsUUID } from 'class-validator'; - -export class DeletePageDto { - @IsUUID() - pageId: string; -} diff --git a/apps/server/src/core/page/dto/history-details.dto.ts b/apps/server/src/core/page/dto/history-details.dto.ts deleted file mode 100644 index da54fcb..0000000 --- a/apps/server/src/core/page/dto/history-details.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsUUID } from 'class-validator'; - -export class HistoryDetailsDto { - @IsUUID() - historyId: string; -} diff --git a/apps/server/src/core/page/dto/page-details.dto.ts b/apps/server/src/core/page/dto/page-details.dto.ts deleted file mode 100644 index f612c3d..0000000 --- a/apps/server/src/core/page/dto/page-details.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsUUID } from 'class-validator'; - -export class PageDetailsDto { - @IsUUID() - pageId: string; -} diff --git a/apps/server/src/core/page/dto/page-history.dto.ts b/apps/server/src/core/page/dto/page-history.dto.ts deleted file mode 100644 index d45c868..0000000 --- a/apps/server/src/core/page/dto/page-history.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsUUID } from 'class-validator'; - -export class PageHistoryDto { - @IsUUID() - pageId: string; -} diff --git a/apps/server/src/core/page/dto/page.dto.ts b/apps/server/src/core/page/dto/page.dto.ts new file mode 100644 index 0000000..be28c27 --- /dev/null +++ b/apps/server/src/core/page/dto/page.dto.ts @@ -0,0 +1,16 @@ +import { IsUUID } from 'class-validator'; + +export class PageIdDto { + @IsUUID() + pageId: string; +} + +export class SpaceIdDto { + @IsUUID() + spaceId: string; +} + +export class PageHistoryIdDto { + @IsUUID() + historyId: string; +} diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 1177a77..3096abc 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -10,16 +10,13 @@ import { PageService } from './services/page.service'; import { CreatePageDto } from './dto/create-page.dto'; import { UpdatePageDto } from './dto/update-page.dto'; import { MovePageDto } from './dto/move-page.dto'; -import { PageDetailsDto } from './dto/page-details.dto'; -import { DeletePageDto } from './dto/delete-page.dto'; +import { PageHistoryIdDto, PageIdDto, SpaceIdDto } from './dto/page.dto'; import { PageOrderingService } from './services/page-ordering.service'; import { PageHistoryService } from './services/page-history.service'; -import { HistoryDetailsDto } from './dto/history-details.dto'; -import { PageHistoryDto } from './dto/page-history.dto'; import { AuthUser } from '../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { User, Workspace } from '@docmost/db/types/entity.types'; @UseGuards(JwtAuthGuard) @@ -33,8 +30,8 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('/info') - async getPage(@Body() input: PageDetailsDto) { - return this.pageService.findById(input.pageId); + async getPage(@Body() pageIdDto: PageIdDto) { + return this.pageService.findById(pageIdDto.pageId); } @HttpCode(HttpStatus.CREATED) @@ -59,13 +56,13 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('delete') - async delete(@Body() deletePageDto: DeletePageDto) { - await this.pageService.forceDelete(deletePageDto.pageId); + async delete(@Body() pageIdDto: PageIdDto) { + await this.pageService.forceDelete(pageIdDto.pageId); } @HttpCode(HttpStatus.OK) @Post('restore') - async restore(@Body() deletePageDto: DeletePageDto) { + async restore(@Body() pageIdDto: PageIdDto) { // await this.pageService.restore(deletePageDto.id); } @@ -78,10 +75,10 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('recent') async getRecentSpacePages( - @Body() { spaceId }, + @Body() spaceIdDto: SpaceIdDto, @Body() pagination: PaginationOptions, ) { - return this.pageService.getRecentSpacePages(spaceId, pagination); + return this.pageService.getRecentSpacePages(spaceIdDto.spaceId, pagination); } @HttpCode(HttpStatus.OK) @@ -106,7 +103,7 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('/history') async getPageHistory( - @Body() dto: PageHistoryDto, + @Body() dto: PageIdDto, @Body() pagination: PaginationOptions, ) { return this.pageHistoryService.findHistoryByPageId(dto.pageId, pagination); @@ -114,7 +111,7 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('/history/details') - async get(@Body() dto: HistoryDetailsDto) { + async get(@Body() dto: PageHistoryIdDto) { return this.pageHistoryService.findById(dto.historyId); } } diff --git a/apps/server/src/core/page/services/page-history.service.ts b/apps/server/src/core/page/services/page-history.service.ts index b1569ca..0ad0d60 100644 --- a/apps/server/src/core/page/services/page-history.service.ts +++ b/apps/server/src/core/page/services/page-history.service.ts @@ -1,9 +1,8 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { Page, PageHistory } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; -import { PaginatedResult } from 'src/helpers/pagination/paginated-result'; -import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class PageHistoryService { @@ -35,14 +34,12 @@ export class PageHistoryService { async findHistoryByPageId( pageId: string, paginationOptions: PaginationOptions, - ) { - const { pageHistory, count } = - await this.pageHistoryRepo.findPageHistoryByPageId( - pageId, - paginationOptions, - ); + ): Promise> { + const pageHistory = await this.pageHistoryRepo.findPageHistoryByPageId( + pageId, + paginationOptions, + ); - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(pageHistory, paginationMeta); + return pageHistory; } } diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 1838a5d..e864fa2 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -11,9 +11,8 @@ import { PageWithOrderingDto } from '../dto/page-with-ordering.dto'; import { transformPageResult } from '../page.util'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { Page } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; -import { PaginationMetaDto } from 'src/helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from 'src/helpers/pagination/paginated-result'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class PageService { @@ -96,6 +95,30 @@ export class PageService { ); } + async getSidebarPagesBySpaceId( + spaceId: string, + limit = 200, + ): Promise { + const pages = await this.pageRepo.getSpaceSidebarPages(spaceId, limit); + return transformPageResult(pages); + } + + async getRecentSpacePages( + spaceId: string, + pagination: PaginationOptions, + ): Promise> { + const pages = await this.pageRepo.getRecentPagesInSpace( + spaceId, + pagination, + ); + + return pages; + } + + async forceDelete(pageId: string): Promise { + await this.pageRepo.deletePage(pageId); + } + /* // TODO: page deletion and restoration async delete(pageId: string): Promise { @@ -186,29 +209,4 @@ export class PageService { } } */ - async forceDelete(pageId: string): Promise { - await this.pageRepo.deletePage(pageId); - } - - async getSidebarPagesBySpaceId( - spaceId: string, - limit = 200, - ): Promise { - const pages = await this.pageRepo.getSpaceSidebarPages(spaceId, limit); - return transformPageResult(pages); - } - - async getRecentSpacePages( - spaceId: string, - paginationOptions: PaginationOptions, - ): Promise> { - const { pages, count } = await this.pageRepo.getRecentPagesInSpace( - spaceId, - paginationOptions, - ); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - - return new PaginatedResult(pages, paginationMeta); - } } diff --git a/apps/server/src/core/space/services/space-member.service.ts b/apps/server/src/core/space/services/space-member.service.ts index 109e80e..e4477ee 100644 --- a/apps/server/src/core/space/services/space-member.service.ts +++ b/apps/server/src/core/space/services/space-member.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; -import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import { KyselyTransaction } from '@docmost/db/types/kysely.types'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { SpaceMember } from '@docmost/db/types/entity.types'; @@ -53,10 +51,29 @@ export class SpaceMemberService { } /* - * get spaces a user is a member of - * either by direct membership or via groups + * get members of a space. + * can be a group or user */ - /* + async getSpaceMembers( + spaceId: string, + workspaceId: string, + pagination: PaginationOptions, + ) { + //todo: validate the space is inside the workspace + const members = await this.spaceMemberRepo.getSpaceMembersPaginated( + spaceId, + pagination, + ); + + return members; + } +} + +/* + * get spaces a user is a member of + * either by direct membership or via groups + */ +/* async getUserSpaces( userId: string, workspaceId: string, @@ -82,25 +99,3 @@ export class SpaceMemberService { return new PaginatedResult(spaces, paginationMeta); } */ - - /* - * get members of a space. - * can be a group or user - */ - async getSpaceMembers( - spaceId: string, - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - //todo: validate the space is inside the workspace - const { members, count } = - await this.spaceMemberRepo.getSpaceMembersPaginated( - spaceId, - paginationOptions, - ); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(members, paginationMeta); - } -} -// 231 lines diff --git a/apps/server/src/core/space/services/space.service.ts b/apps/server/src/core/space/services/space.service.ts index d83c07a..b5f694e 100644 --- a/apps/server/src/core/space/services/space.service.ts +++ b/apps/server/src/core/space/services/space.service.ts @@ -4,13 +4,12 @@ import { NotFoundException, } from '@nestjs/common'; import { CreateSpaceDto } from '../dto/create-space.dto'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; -import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import slugify from 'slugify'; import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; import { KyselyTransaction } from '@docmost/db/types/kysely.types'; import { Space } from '@docmost/db/types/entity.types'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class SpaceService { @@ -46,7 +45,6 @@ export class SpaceService { } async getSpaceInfo(spaceId: string, workspaceId: string): Promise { - // TODO: add memberCount const space = await this.spaceRepo.findById(spaceId, workspaceId); if (!space) { throw new NotFoundException('Space not found'); @@ -57,15 +55,13 @@ export class SpaceService { async getWorkspaceSpaces( workspaceId: string, - paginationOptions: PaginationOptions, - ): Promise> { - const { spaces, count } = await this.spaceRepo.getSpacesInWorkspace( + pagination: PaginationOptions, + ): Promise> { + const spaces = await this.spaceRepo.getSpacesInWorkspace( workspaceId, - paginationOptions, + pagination, ); - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - - return new PaginatedResult(spaces, paginationMeta); + return spaces; } } diff --git a/apps/server/src/core/space/space.controller.ts b/apps/server/src/core/space/space.controller.ts index ea33a87..45ff1f0 100644 --- a/apps/server/src/core/space/space.controller.ts +++ b/apps/server/src/core/space/space.controller.ts @@ -11,7 +11,7 @@ import { AuthUser } from '../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { SpaceIdDto } from './dto/space-id.dto'; -import { PaginationOptions } from '../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../kysely/pagination/pagination-options'; import { SpaceMemberService } from './services/space-member.service'; import { User, Workspace } from '@docmost/db/types/entity.types'; diff --git a/apps/server/src/core/workspace/controllers/workspace.controller.ts b/apps/server/src/core/workspace/controllers/workspace.controller.ts index 3ddd290..1dfcb86 100644 --- a/apps/server/src/core/workspace/controllers/workspace.controller.ts +++ b/apps/server/src/core/workspace/controllers/workspace.controller.ts @@ -12,7 +12,7 @@ import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto'; import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto'; import { AuthUser } from '../../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import { WorkspaceInvitationService } from '../services/workspace-invitation.service'; import { Public } from '../../../decorators/public.decorator'; import { diff --git a/apps/server/src/core/workspace/services/workspace-user.service.ts b/apps/server/src/core/workspace/services/workspace-user.service.ts index 74b8aaa..5f5e70f 100644 --- a/apps/server/src/core/workspace/services/workspace-user.service.ts +++ b/apps/server/src/core/workspace/services/workspace-user.service.ts @@ -1,11 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; -import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { PaginationOptions } from '../../../kysely/pagination/pagination-options'; import { UserRole } from '../../../helpers/types/permission'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; import { User } from '@docmost/db/types/entity.types'; +import { PaginationResult } from '@docmost/db/pagination/pagination'; @Injectable() export class WorkspaceUserService { @@ -13,15 +12,14 @@ export class WorkspaceUserService { async getWorkspaceUsers( workspaceId: string, - paginationOptions: PaginationOptions, - ): Promise> { - const { users, count } = await this.userRepo.getUsersPaginated( + pagination: PaginationOptions, + ): Promise> { + const users = await this.userRepo.getUsersPaginated( workspaceId, - paginationOptions, + pagination, ); - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(users, paginationMeta); + return users; } async updateWorkspaceUserRole( diff --git a/apps/server/src/helpers/pagination/paginated-result.ts b/apps/server/src/helpers/pagination/paginated-result.ts deleted file mode 100644 index e41aae5..0000000 --- a/apps/server/src/helpers/pagination/paginated-result.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IsArray } from 'class-validator'; -import { PaginationMetaDto } from './pagination-meta-dto'; - -export class PaginatedResult { - @IsArray() - readonly items: T[]; - - readonly pagination: PaginationMetaDto; - - constructor(items: T[], pagination: PaginationMetaDto) { - this.items = items; - this.pagination = pagination; - } -} diff --git a/apps/server/src/helpers/pagination/pagination-meta-dto.ts b/apps/server/src/helpers/pagination/pagination-meta-dto.ts deleted file mode 100644 index 9991bed..0000000 --- a/apps/server/src/helpers/pagination/pagination-meta-dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PaginationOptions } from './pagination-options'; - -export class PaginationMetaDto { - readonly page: number; - - readonly limit: number; - - readonly total: number; - - readonly pageCount: number; - - readonly hasPreviousPage: boolean; - - readonly hasNextPage: boolean; - - constructor({ count, paginationOptions }: PageMetaDtoParameters) { - this.page = paginationOptions.page; - this.limit = paginationOptions.limit; - this.total = count; - this.pageCount = Math.ceil(this.total / this.limit); - this.hasPreviousPage = this.page > 1; - this.hasNextPage = this.page < this.pageCount; - } -} - -export interface PageMetaDtoParameters { - count: number; - paginationOptions: PaginationOptions; -} diff --git a/apps/server/src/kysely/kysely-db.module.ts b/apps/server/src/kysely/kysely-db.module.ts index 24fe2fb..a4ad950 100644 --- a/apps/server/src/kysely/kysely-db.module.ts +++ b/apps/server/src/kysely/kysely-db.module.ts @@ -2,7 +2,7 @@ import { Global, Module } from '@nestjs/common'; import { KyselyModule } from 'nestjs-kysely'; import { EnvironmentService } from '../integrations/environment/environment.service'; import { CamelCasePlugin, LogEvent, PostgresDialect } from 'kysely'; -import { Pool } from 'pg'; +import { Pool, types } from 'pg'; import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; @@ -15,6 +15,9 @@ import { PageHistoryRepo } from './repos/page/page-history.repo'; import { PageOrderingRepo } from './repos/page/page-ordering.repo'; import { AttachmentRepo } from './repos/attachment/attachment.repo'; +// https://github.com/brianc/node-postgres/issues/811 +types.setTypeParser(types.builtins.INT8, (val) => Number(val)); + @Global() @Module({ imports: [ diff --git a/apps/server/src/helpers/pagination/pagination-options.ts b/apps/server/src/kysely/pagination/pagination-options.ts similarity index 52% rename from apps/server/src/helpers/pagination/pagination-options.ts rename to apps/server/src/kysely/pagination/pagination-options.ts index f0de192..73f6e38 100644 --- a/apps/server/src/helpers/pagination/pagination-options.ts +++ b/apps/server/src/kysely/pagination/pagination-options.ts @@ -1,4 +1,11 @@ -import { IsNumber, IsOptional, IsPositive, Max, Min } from 'class-validator'; +import { + IsNumber, + IsOptional, + IsPositive, + IsString, + Max, + Min, +} from 'class-validator'; export class PaginationOptions { @IsOptional() @@ -14,15 +21,15 @@ export class PaginationOptions { limit = 20; @IsOptional() - @IsNumber() - offset = 0; + @IsString() + query: string; - get skip(): number { + get offset(): number { return (this.page - 1) * this.limit; } } -export enum Order { - ASC = 'ASC', - DESC = 'DESC', +export enum PaginationSort { + ASC = 'asc', + DESC = 'desc', } diff --git a/apps/server/src/kysely/pagination/pagination.ts b/apps/server/src/kysely/pagination/pagination.ts new file mode 100644 index 0000000..e4f7f23 --- /dev/null +++ b/apps/server/src/kysely/pagination/pagination.ts @@ -0,0 +1,66 @@ +// adapted from https://github.com/charlie-hadden/kysely-paginate/blob/main/src/offset.ts - MIT +import { SelectQueryBuilder, StringReference, sql } from 'kysely'; + +export type PaginationMeta = { + limit: number; + page: number; + hasNextPage: boolean; + hasPrevPage: boolean; +}; +export type PaginationResult = { + items: T[]; + meta: PaginationMeta; +}; + +export async function executeWithPagination( + qb: SelectQueryBuilder, + opts: { + perPage: number; + page: number; + experimental_deferredJoinPrimaryKey?: StringReference; + }, +): Promise> { + qb = qb.limit(opts.perPage + 1).offset((opts.page - 1) * opts.perPage); + + const deferredJoinPrimaryKey = opts.experimental_deferredJoinPrimaryKey; + + if (deferredJoinPrimaryKey) { + const primaryKeys = await qb + .clearSelect() + .select((eb) => eb.ref(deferredJoinPrimaryKey).as('primaryKey')) + .execute() + // @ts-expect-error TODO: Fix the type here later + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + .then((rows) => rows.map((row) => row.primaryKey)); + + qb = qb + .where((eb) => + primaryKeys.length > 0 + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + eb(deferredJoinPrimaryKey, 'in', primaryKeys as any) + : eb(sql`1`, '=', 0), + ) + .clearOffset() + .clearLimit(); + } + + const rows = await qb.execute(); + const hasNextPage = rows.length > 0 ? rows.length > opts.perPage : false; + const hasPrevPage = rows.length > 0 ? opts.page > 1 : false; + + // If we fetched an extra row to determine if we have a next page, that + // shouldn't be in the returned results + if (rows.length > opts.perPage) { + rows.pop(); + } + + return { + items: rows, + meta: { + limit: opts.perPage, + page: opts.page, + hasNextPage, + hasPrevPage, + }, + }; +} diff --git a/apps/server/src/kysely/repos/attachment/attachment.repo.ts b/apps/server/src/kysely/repos/attachment/attachment.repo.ts index d09e3f6..f3bda7b 100644 --- a/apps/server/src/kysely/repos/attachment/attachment.repo.ts +++ b/apps/server/src/kysely/repos/attachment/attachment.repo.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { Attachment, InsertableAttachment, @@ -28,17 +28,13 @@ export class AttachmentRepo { insertableAttachment: InsertableAttachment, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('attachments') - .values(insertableAttachment) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + + return db + .insertInto('attachments') + .values(insertableAttachment) + .returningAll() + .executeTakeFirst(); } async updateAttachment( diff --git a/apps/server/src/kysely/repos/comment/comment.repo.ts b/apps/server/src/kysely/repos/comment/comment.repo.ts index 04ea6d6..6646f90 100644 --- a/apps/server/src/kysely/repos/comment/comment.repo.ts +++ b/apps/server/src/kysely/repos/comment/comment.repo.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; -import { executeTx } from '../../utils'; +import { dbOrTx } from '../../utils'; import { Comment, InsertableComment, UpdatableComment, } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class CommentRepo { @@ -22,26 +23,19 @@ export class CommentRepo { .executeTakeFirst(); } - async findPageComments(pageId: string, paginationOptions: PaginationOptions) { - return executeTx(this.db, async (trx) => { - const comments = await trx - .selectFrom('comments') - .selectAll() - .where('pageId', '=', pageId) - .orderBy('createdAt', 'asc') - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + async findPageComments(pageId: string, pagination: PaginationOptions) { + const query = this.db + .selectFrom('comments') + .selectAll() + .where('pageId', '=', pageId) + .orderBy('createdAt', 'asc'); - let { count } = await trx - .selectFrom('comments') - .select((eb) => eb.fn.count('id').as('count')) - .where('pageId', '=', pageId) - .executeTakeFirst(); - - count = count as number; - return { comments, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; } async updateComment( @@ -49,34 +43,25 @@ export class CommentRepo { commentId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .updateTable('comments') - .set(updatableComment) - .where('id', '=', commentId) - .execute(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + + db.updateTable('comments') + .set(updatableComment) + .where('id', '=', commentId) + .execute(); } async insertComment( insertableComment: InsertableComment, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('comments') - .values(insertableComment) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + + return db + .insertInto('comments') + .values(insertableComment) + .returningAll() + .executeTakeFirst(); } async deleteComment(commentId: string): Promise { diff --git a/apps/server/src/kysely/repos/group/group-user.repo.ts b/apps/server/src/kysely/repos/group/group-user.repo.ts index 9ae9200..1bbefcc 100644 --- a/apps/server/src/kysely/repos/group/group-user.repo.ts +++ b/apps/server/src/kysely/repos/group/group-user.repo.ts @@ -1,14 +1,15 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { GroupUser, InsertableGroupUser, User, } from '@docmost/db/types/entity.types'; import { sql } from 'kysely'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../pagination/pagination-options'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class GroupUserRepo { @@ -19,67 +20,47 @@ export class GroupUserRepo { groupId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .selectFrom('groupUsers') - .selectAll() - .where('userId', '=', userId) - .where('groupId', '=', groupId) - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .selectFrom('groupUsers') + .selectAll() + .where('userId', '=', userId) + .where('groupId', '=', groupId) + .executeTakeFirst(); } async insertGroupUser( insertableGroupUser: InsertableGroupUser, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('groupUsers') - .values(insertableGroupUser) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('groupUsers') + .values(insertableGroupUser) + .returningAll() + .executeTakeFirst(); } - async getGroupUsersPaginated( - groupId: string, - paginationOptions: PaginationOptions, - ): Promise<{ users: User[]; count: number }> { - // todo add group member count - return executeTx(this.db, async (trx) => { - const groupUsers = (await trx - .selectFrom('groupUsers') - .innerJoin('users', 'users.id', 'groupUsers.userId') - .select(sql`users.*` as any) - .where('groupId', '=', groupId) - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute()) as User[]; + async getGroupUsersPaginated(groupId: string, pagination: PaginationOptions) { + let query = this.db + .selectFrom('groupUsers') + .innerJoin('users', 'users.id', 'groupUsers.userId') + .select(sql`users.*` as any) + .where('groupId', '=', groupId) + .orderBy('createdAt', 'asc'); - const users: User[] = groupUsers.map((user: User) => { - delete user.password; - return user; - }); + if (pagination.query) { + query = query.where((eb) => + eb('users.name', 'ilike', `%${pagination.query}%`), + ); + } - let { count } = await trx - .selectFrom('groupUsers') - .select((eb) => eb.fn.count('id').as('count')) - .where('groupId', '=', groupId) - .executeTakeFirst(); - - count = count as number; - - return { users, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; } async delete(userId: string, groupId: string): Promise { diff --git a/apps/server/src/kysely/repos/group/group.repo.ts b/apps/server/src/kysely/repos/group/group.repo.ts index e065dc4..2e7c627 100644 --- a/apps/server/src/kysely/repos/group/group.repo.ts +++ b/apps/server/src/kysely/repos/group/group.repo.ts @@ -1,23 +1,44 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { Group, InsertableGroup, UpdatableGroup, } from '@docmost/db/types/entity.types'; -import { sql } from 'kysely'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { ExpressionBuilder, sql } from 'kysely'; +import { PaginationOptions } from '../../pagination/pagination-options'; +import { DB } from '@docmost/db/types/db'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class GroupRepo { constructor(@InjectKysely() private readonly db: KyselyDB) {} + private baseFields: Array = [ + 'id', + 'name', + 'description', + 'isDefault', + 'workspaceId', + 'creatorId', + 'createdAt', + 'updatedAt', + ]; + + countGroupMembers(eb: ExpressionBuilder) { + return eb + .selectFrom('groupUsers') + .select((eb) => eb.fn.countAll().as('count')) + .whereRef('groupUsers.groupId', '=', 'groups.id') + .as('memberCount'); + } + async findById(groupId: string, workspaceId: string): Promise { return await this.db .selectFrom('groups') - .selectAll() + .select((eb) => [...this.baseFields, this.countGroupMembers(eb)]) .where('id', '=', groupId) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); @@ -26,7 +47,7 @@ export class GroupRepo { async findByName(groupName: string, workspaceId: string): Promise { return await this.db .selectFrom('groups') - .selectAll() + .select((eb) => [...this.baseFields, this.countGroupMembers(eb)]) .where(sql`LOWER(name)`, '=', sql`LOWER(${groupName})`) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); @@ -49,60 +70,50 @@ export class GroupRepo { insertableGroup: InsertableGroup, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('groups') - .values(insertableGroup) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('groups') + .values(insertableGroup) + .returningAll() + .executeTakeFirst(); } async getDefaultGroup( workspaceId: string, trx: KyselyTransaction, ): Promise { - return executeTx( - this.db, - async (trx) => { - return await trx - .selectFrom('groups') - .selectAll() - .where('isDefault', '=', true) - .where('workspaceId', '=', workspaceId) - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .selectFrom('groups') + .select((eb) => [...this.baseFields, this.countGroupMembers(eb)]) + .where('isDefault', '=', true) + .where('workspaceId', '=', workspaceId) + .executeTakeFirst(); } - async getGroupsPaginated( - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - // todo add group member count - return executeTx(this.db, async (trx) => { - const groups = await trx - .selectFrom('groups') - .selectAll() - .where('workspaceId', '=', workspaceId) - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) { + let query = this.db + .selectFrom('groups') + .select((eb) => [...this.baseFields, this.countGroupMembers(eb)]) + .where('workspaceId', '=', workspaceId) + .orderBy('createdAt', 'asc'); - let { count } = await trx - .selectFrom('groups') - .select((eb) => eb.fn.count('id').as('count')) - .where('workspaceId', '=', workspaceId) - .executeTakeFirst(); + if (pagination.query) { + query = query.where((eb) => + eb('name', 'ilike', `%${pagination.query}%`).or( + 'description', + 'ilike', + `%${pagination.query}%`, + ), + ); + } - count = count as number; - return { groups, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; } async delete(groupId: string, workspaceId: string): Promise { diff --git a/apps/server/src/kysely/repos/page/page-history.repo.ts b/apps/server/src/kysely/repos/page/page-history.repo.ts index 72a942d..d62242a 100644 --- a/apps/server/src/kysely/repos/page/page-history.repo.ts +++ b/apps/server/src/kysely/repos/page/page-history.repo.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; -import { executeTx } from '../../utils'; +import { dbOrTx } from '../../utils'; import { InsertablePageHistory, PageHistory, UpdatablePageHistory, } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class PageHistoryRepo { @@ -26,74 +27,55 @@ export class PageHistoryRepo { pageHistoryId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .updateTable('pageHistory') - .set(updatablePageHistory) - .where('id', '=', pageHistoryId) - .execute(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .updateTable('pageHistory') + .set(updatablePageHistory) + .where('id', '=', pageHistoryId) + .execute(); } async insertPageHistory( insertablePageHistory: InsertablePageHistory, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('pageHistory') - .values(insertablePageHistory) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('pageHistory') + .values(insertablePageHistory) + .returningAll() + .executeTakeFirst(); } - async findPageHistoryByPageId( - pageId: string, - paginationOptions: PaginationOptions, - ) { - return executeTx(this.db, async (trx) => { - const pageHistory = await trx - .selectFrom('pageHistory as history') - .innerJoin('users as user', 'user.id', 'history.lastUpdatedById') - .select([ - 'history.id', - 'history.pageId', - 'history.title', - 'history.slug', - 'history.icon', - 'history.coverPhoto', - 'history.version', - 'history.lastUpdatedById', - 'history.workspaceId', - 'history.createdAt', - 'history.updatedAt', - 'user.id', - 'user.name', - 'user.avatarUrl', - ]) - .where('pageId', '=', pageId) - .orderBy('createdAt', 'desc') - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) { + // todo: fix user relationship + const query = this.db + .selectFrom('pageHistory as history') + .innerJoin('users as user', 'user.id', 'history.lastUpdatedById') + .select([ + 'history.id', + 'history.pageId', + 'history.title', + 'history.slug', + 'history.icon', + 'history.coverPhoto', + 'history.version', + 'history.lastUpdatedById', + 'history.workspaceId', + 'history.createdAt', + 'history.updatedAt', + 'user.id', + 'user.name', + 'user.avatarUrl', + ]) + .where('pageId', '=', pageId) + .orderBy('createdAt', 'desc'); - let { count } = await trx - .selectFrom('pageHistory') - .select((eb) => eb.fn.count('id').as('count')) - .where('pageId', '=', pageId) - .executeTakeFirst(); - - count = count as number; - return { pageHistory, count }; + const result = executeWithPagination(query, { + page: pagination.offset, + perPage: pagination.limit, }); + + return result; } } diff --git a/apps/server/src/kysely/repos/page/page-ordering.repo.ts b/apps/server/src/kysely/repos/page/page-ordering.repo.ts index cabbc6e..07e295f 100644 --- a/apps/server/src/kysely/repos/page/page-ordering.repo.ts +++ b/apps/server/src/kysely/repos/page/page-ordering.repo.ts @@ -1,66 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; -import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; -import { executeTx } from '../../utils'; -import { - InsertablePage, - Page, - UpdatablePage, -} from '@docmost/db/types/entity.types'; -import { sql } from 'kysely'; +import { KyselyDB } from '../../types/kysely.types'; @Injectable() export class PageOrderingRepo { constructor(@InjectKysely() private readonly db: KyselyDB) {} - - async findById(pageId: string): Promise { - return await this.db - .selectFrom('pages') - .selectAll() - .where('id', '=', pageId) - .executeTakeFirst(); - } - - async slug(slug: string): Promise { - return await this.db - .selectFrom('pages') - .selectAll() - .where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`) - .executeTakeFirst(); - } - - async updatePage( - updatablePage: UpdatablePage, - pageId: string, - trx?: KyselyTransaction, - ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .updateTable('pages') - .set(updatablePage) - .where('id', '=', pageId) - .execute(); - }, - trx, - ); - } - - async insertPage( - insertablePage: InsertablePage, - trx?: KyselyTransaction, - ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('pages') - .values(insertablePage) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); - } } diff --git a/apps/server/src/kysely/repos/page/page.repo.ts b/apps/server/src/kysely/repos/page/page.repo.ts index 6191761..a786c3f 100644 --- a/apps/server/src/kysely/repos/page/page.repo.ts +++ b/apps/server/src/kysely/repos/page/page.repo.ts @@ -1,15 +1,16 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; -import { executeTx } from '../../utils'; +import { dbOrTx } from '../../utils'; import { InsertablePage, Page, UpdatablePage, } from '@docmost/db/types/entity.types'; import { sql } from 'kysely'; -import { PaginationOptions } from 'src/helpers/pagination/pagination-options'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { OrderingEntity } from 'src/core/page/page.util'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; // TODO: scope to space/workspace @Injectable() @@ -63,63 +64,43 @@ export class PageRepo { pageId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .updateTable('pages') - .set(updatablePage) - .where('id', '=', pageId) - .execute(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .updateTable('pages') + .set(updatablePage) + .where('id', '=', pageId) + .execute(); } async insertPage( insertablePage: InsertablePage, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('pages') - .values(insertablePage) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('pages') + .values(insertablePage) + .returningAll() + .executeTakeFirst(); } async deletePage(pageId: string): Promise { await this.db.deleteFrom('pages').where('id', '=', pageId).execute(); } - async getRecentPagesInSpace( - spaceId: string, - paginationOptions: PaginationOptions, - ) { - return executeTx(this.db, async (trx) => { - const pages = await trx - .selectFrom('pages') - .select(this.baseFields) - .where('spaceId', '=', spaceId) - .orderBy('updatedAt', 'desc') - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + async getRecentPagesInSpace(spaceId: string, pagination: PaginationOptions) { + const query = this.db + .selectFrom('pages') + .select(this.baseFields) + .where('spaceId', '=', spaceId) + .orderBy('updatedAt', 'desc'); - let { count } = await trx - .selectFrom('pages') - .select((eb) => eb.fn.count('id').as('count')) - .where('spaceId', '=', spaceId) - .executeTakeFirst(); - - count = count as number; - return { pages, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; } async getSpaceSidebarPages(spaceId: string, limit: number) { @@ -138,8 +119,7 @@ export class PageRepo { 'page.creatorId', 'page.createdAt', ]) - .orderBy('page.createdAt', 'desc') - .orderBy('updatedAt', 'desc') + .orderBy('page.updatedAt', 'desc') .limit(limit) .execute(); diff --git a/apps/server/src/kysely/repos/space/space-member.repo.ts b/apps/server/src/kysely/repos/space/space-member.repo.ts index c550a9e..f22df04 100644 --- a/apps/server/src/kysely/repos/space/space-member.repo.ts +++ b/apps/server/src/kysely/repos/space/space-member.repo.ts @@ -1,14 +1,15 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { InsertableSpaceMember, SpaceMember, } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../pagination/pagination-options'; import { MemberInfo } from './types'; import { sql } from 'kysely'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class SpaceMemberRepo { @@ -18,83 +19,70 @@ export class SpaceMemberRepo { insertableSpaceMember: InsertableSpaceMember, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('spaceMembers') - .values(insertableSpaceMember) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('spaceMembers') + .values(insertableSpaceMember) + .returningAll() + .executeTakeFirst(); } async getSpaceMembersPaginated( spaceId: string, - paginationOptions: PaginationOptions, + pagination: PaginationOptions, ) { - return executeTx(this.db, async (trx) => { - const spaceMembers = await trx - .selectFrom('spaceMembers') - .leftJoin('users', 'users.id', 'spaceMembers.userId') - .leftJoin('groups', 'groups.id', 'spaceMembers.groupId') - .select([ - 'groups.id as group_id', - 'groups.name as group_name', - 'groups.isDefault as group_isDefault', - 'groups.id as groups_id', - 'groups.id as groups_id', - 'groups.id as groups_id', - 'users.id as user_id', - 'users.name as user_name', - 'users.avatarUrl as user_avatarUrl', - 'users.email as user_email', - 'spaceMembers.role', - ]) - .where('spaceId', '=', spaceId) - .orderBy('spaceMembers.createdAt', 'asc') - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + const query = this.db + .selectFrom('spaceMembers') + .leftJoin('users', 'users.id', 'spaceMembers.userId') + .leftJoin('groups', 'groups.id', 'spaceMembers.groupId') + .select([ + 'groups.id as group_id', + 'groups.name as group_name', + 'groups.isDefault as group_isDefault', + 'groups.id as groups_id', + 'groups.id as groups_id', + 'groups.id as groups_id', + 'users.id as user_id', + 'users.name as user_name', + 'users.avatarUrl as user_avatarUrl', + 'users.email as user_email', + 'spaceMembers.role', + ]) + .where('spaceId', '=', spaceId) + .orderBy('spaceMembers.createdAt', 'asc'); - let memberInfo: MemberInfo; - - const members = spaceMembers.map((member) => { - if (member.user_id) { - memberInfo = { - id: member.user_id, - name: member.user_name, - email: member.user_email, - avatarUrl: member.user_avatarUrl, - type: 'user', - }; - } else if (member.group_id) { - memberInfo = { - id: member.group_id, - name: member.group_name, - isDefault: member.group_isDefault, - type: 'group', - }; - // todo: member count - } - - return { - ...memberInfo, - role: member.role, - }; - }); - - let { count } = await trx - .selectFrom('spaceMembers') - .select((eb) => eb.fn.count('id').as('count')) - .where('spaceId', '=', spaceId) - .executeTakeFirst(); - count = count as number; - - return { members, count }; + const result = await executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + let memberInfo: MemberInfo; + + const members = result.items.map((member) => { + if (member.user_id) { + memberInfo = { + id: member.user_id, + name: member.user_name, + email: member.user_email, + avatarUrl: member.user_avatarUrl, + type: 'user', + }; + } else if (member.group_id) { + memberInfo = { + id: member.group_id, + name: member.group_name, + isDefault: member.group_isDefault, + type: 'group', + }; + } + + return { + ...memberInfo, + role: member.role, + }; + }); + + return members; } /* @@ -263,18 +251,13 @@ export class SpaceMemberRepo { groupId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .selectFrom('spaceMembers') - .selectAll() - .where('userId', '=', userId) - .where('groupId', '=', groupId) - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .selectFrom('spaceMembers') + .selectAll() + .where('userId', '=', userId) + .where('groupId', '=', groupId) + .executeTakeFirst(); } async removeUser(userId: string, spaceId: string): Promise { diff --git a/apps/server/src/kysely/repos/space/space.repo.ts b/apps/server/src/kysely/repos/space/space.repo.ts index 3593888..a24cf6f 100644 --- a/apps/server/src/kysely/repos/space/space.repo.ts +++ b/apps/server/src/kysely/repos/space/space.repo.ts @@ -1,23 +1,40 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { InsertableSpace, Space, UpdatableSpace, } from '@docmost/db/types/entity.types'; -import { sql } from 'kysely'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { ExpressionBuilder, sql } from 'kysely'; +import { PaginationOptions } from '../../pagination/pagination-options'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; +import { DB } from '@docmost/db/types/db'; @Injectable() export class SpaceRepo { constructor(@InjectKysely() private readonly db: KyselyDB) {} + private baseFields: Array = [ + 'id', + 'name', + 'description', + 'slug', + 'icon', + 'visibility', + 'defaultRole', + 'workspaceId', + 'creatorId', + 'createdAt', + 'updatedAt', + 'deletedAt', + ]; + async findById(spaceId: string, workspaceId: string): Promise { return await this.db .selectFrom('spaces') - .selectAll() + .select((eb) => [...this.baseFields, this.countSpaceMembers(eb)]) .where('id', '=', spaceId) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); @@ -26,7 +43,7 @@ export class SpaceRepo { async findBySlug(slug: string, workspaceId: string): Promise { return await this.db .selectFrom('spaces') - .selectAll() + .select((eb) => [...this.baseFields, this.countSpaceMembers(eb)]) .where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); @@ -37,28 +54,25 @@ export class SpaceRepo { workspaceId: string, trx?: KyselyTransaction, ): Promise { - return executeTx( - this.db, - async (trx) => { - let { count } = await trx - .selectFrom('spaces') - .select((eb) => eb.fn.count('id').as('count')) - .where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`) - .where('workspaceId', '=', workspaceId) - .executeTakeFirst(); - count = count as number; - return count == 0 ? false : true; - }, - trx, - ); + const db = dbOrTx(this.db, trx); + let { count } = await db + .selectFrom('spaces') + .select((eb) => eb.fn.count('id').as('count')) + .where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`) + .where('workspaceId', '=', workspaceId) + .executeTakeFirst(); + count = count as number; + return count == 0 ? false : true; } async updateSpace( updatableSpace: UpdatableSpace, spaceId: string, workspaceId: string, + trx?: KyselyTransaction, ) { - return await this.db + const db = dbOrTx(this.db, trx); + return db .updateTable('spaces') .set(updatableSpace) .where('id', '=', spaceId) @@ -70,44 +84,50 @@ export class SpaceRepo { insertableSpace: InsertableSpace, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('spaces') - .values(insertableSpace) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('spaces') + .values(insertableSpace) + .returningAll() + .executeTakeFirst(); } async getSpacesInWorkspace( workspaceId: string, - paginationOptions: PaginationOptions, + pagination: PaginationOptions, ) { - //todo: add member count - // to: show spaces user have access based on visibility and membership + // todo: show spaces user have access based on visibility and memberships + let query = this.db + .selectFrom('spaces') + .select((eb) => [...this.baseFields, this.countSpaceMembers(eb)]) + .where('workspaceId', '=', workspaceId) + .orderBy('createdAt', 'asc'); - return executeTx(this.db, async (trx) => { - const spaces = await trx - .selectFrom('spaces') - .selectAll() - .where('workspaceId', '=', workspaceId) - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + if (pagination.query) { + query = query.where((eb) => + eb('name', 'ilike', `%${pagination.query}%`).or( + 'description', + 'ilike', + `%${pagination.query}%`, + ), + ); + } - let { count } = await trx - .selectFrom('spaces') - .select((eb) => eb.fn.count('id').as('count')) - .where('workspaceId', '=', workspaceId) - .executeTakeFirst(); - - count = count as number; - return { spaces, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; + } + + countSpaceMembers(eb: ExpressionBuilder) { + // should get unique members via groups? + return eb + .selectFrom('spaceMembers') + .select((eb) => eb.fn.countAll().as('count')) + .whereRef('spaceMembers.spaceId', '=', 'spaces.id') + .as('memberCount'); } async deleteSpace(spaceId: string, workspaceId: string): Promise { diff --git a/apps/server/src/kysely/repos/user/user.repo.ts b/apps/server/src/kysely/repos/user/user.repo.ts index 0397d93..c2b5b25 100644 --- a/apps/server/src/kysely/repos/user/user.repo.ts +++ b/apps/server/src/kysely/repos/user/user.repo.ts @@ -3,13 +3,14 @@ import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { Users } from '@docmost/db/types/db'; import { hashPassword } from '../../../helpers/utils'; -import { executeTx } from '@docmost/db/utils'; +import { dbOrTx } from '@docmost/db/utils'; import { InsertableUser, UpdatableUser, User, } from '@docmost/db/types/entity.types'; -import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationOptions } from '../../pagination/pagination-options'; +import { executeWithPagination } from '@docmost/db/pagination/pagination'; @Injectable() export class UserRepo { @@ -35,8 +36,10 @@ export class UserRepo { userId: string, workspaceId: string, includePassword?: boolean, + trx?: KyselyTransaction, ): Promise { - return this.db + const db = dbOrTx(this.db, trx); + return db .selectFrom('users') .select(this.baseFields) .$if(includePassword, (qb) => qb.select('password')) @@ -95,17 +98,12 @@ export class UserRepo { lastLoginAt: new Date(), }; - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('users') - .values(user) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('users') + .values(user) + .returningAll() + .executeTakeFirst(); } async roleCountByWorkspaceId( @@ -122,28 +120,24 @@ export class UserRepo { return count as number; } - async getUsersPaginated( - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - return executeTx(this.db, async (trx) => { - const users = await trx - .selectFrom('users') - .select(this.baseFields) - .where('workspaceId', '=', workspaceId) - .orderBy('createdAt asc') - .limit(paginationOptions.limit) - .offset(paginationOptions.offset) - .execute(); + async getUsersPaginated(workspaceId: string, pagination: PaginationOptions) { + let query = this.db + .selectFrom('users') + .select(this.baseFields) + .where('workspaceId', '=', workspaceId) + .orderBy('createdAt', 'asc'); - let { count } = await trx - .selectFrom('users') - .select((eb) => eb.fn.countAll().as('count')) - .where('workspaceId', '=', workspaceId) - .executeTakeFirst(); + if (pagination.query) { + query = query.where((eb) => + eb('users.name', 'ilike', `%${pagination.query}%`), + ); + } - count = count as number; - return { users, count }; + const result = executeWithPagination(query, { + page: pagination.page, + perPage: pagination.limit, }); + + return result; } } diff --git a/apps/server/src/kysely/repos/workspace/workspace.repo.ts b/apps/server/src/kysely/repos/workspace/workspace.repo.ts index 39e371c..17a5830 100644 --- a/apps/server/src/kysely/repos/workspace/workspace.repo.ts +++ b/apps/server/src/kysely/repos/workspace/workspace.repo.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '../../types/kysely.types'; -import { executeTx } from '../../utils'; +import { dbOrTx } from '../../utils'; import { InsertableWorkspace, UpdatableWorkspace, @@ -43,34 +43,24 @@ export class WorkspaceRepo { workspaceId: string, trx?: KyselyTransaction, ) { - return await executeTx( - this.db, - async (trx) => { - return await trx - .updateTable('workspaces') - .set(updatableWorkspace) - .where('id', '=', workspaceId) - .execute(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .updateTable('workspaces') + .set(updatableWorkspace) + .where('id', '=', workspaceId) + .execute(); } async insertWorkspace( insertableWorkspace: InsertableWorkspace, trx?: KyselyTransaction, ): Promise { - return await executeTx( - this.db, - async (trx) => { - return await trx - .insertInto('workspaces') - .values(insertableWorkspace) - .returningAll() - .executeTakeFirst(); - }, - trx, - ); + const db = dbOrTx(this.db, trx); + return db + .insertInto('workspaces') + .values(insertableWorkspace) + .returningAll() + .executeTakeFirst(); } async count(): Promise { diff --git a/apps/server/src/kysely/types/entity.types.ts b/apps/server/src/kysely/types/entity.types.ts index 069b8db..59cdd3d 100644 --- a/apps/server/src/kysely/types/entity.types.ts +++ b/apps/server/src/kysely/types/entity.types.ts @@ -42,6 +42,8 @@ export type InsertableSpaceMember = Insertable; export type UpdatableSpaceMember = Updateable>; // Group +export type ExtendedGroup = Groups & { memberCount: number }; + export type Group = Selectable; export type InsertableGroup = Insertable; export type UpdatableGroup = Updateable>; diff --git a/apps/server/src/kysely/utils.ts b/apps/server/src/kysely/utils.ts index ecf8d06..6c11339 100644 --- a/apps/server/src/kysely/utils.ts +++ b/apps/server/src/kysely/utils.ts @@ -1,13 +1,33 @@ import { KyselyDB, KyselyTransaction } from './types/kysely.types'; +/* + * Executes a transaction or a callback using the provided database instance. + * If an existing transaction is provided, it directly executes the callback with it. + * Otherwise, it starts a new transaction using the provided database instance and executes the callback within that transaction. + */ export async function executeTx( db: KyselyDB, callback: (trx: KyselyTransaction) => Promise, existingTrx?: KyselyTransaction, ): Promise { if (existingTrx) { - return await callback(existingTrx); + return await callback(existingTrx); // Execute callback with existing transaction } else { - return await db.transaction().execute((trx) => callback(trx)); + return await db.transaction().execute((trx) => callback(trx)); // Start new transaction and execute callback + } +} + +/* + * This function returns either an existing transaction if provided, + * or the normal database instance. + */ +export function dbOrTx( + db: KyselyDB, + existingTrx?: KyselyTransaction, +): KyselyDB | KyselyTransaction { + if (existingTrx) { + return existingTrx; // Use existing transaction + } else { + return db; // Use normal database instance } }