From 3473ff85ed7b1da5ef48ba62740d28c783212945 Mon Sep 17 00:00:00 2001 From: Zetro Date: Sat, 1 Mar 2025 14:25:48 +0100 Subject: [PATCH 1/2] fix(invite): refactor acceptInvite method to execute updates in a transactions --- src/modules/invite/invite.service.ts | 31 ++++++++++++------- .../organisations/organisations.service.ts | 11 +++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 6df36001d..4b75d2de4 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -2,7 +2,7 @@ import { HttpStatus, Injectable, InternalServerErrorException, NotFoundException import { InviteDto } from './dto/invite.dto'; import { Invite } from './entities/invite.entity'; import { Organisation } from '@modules/organisations/entities/organisations.entity'; -import { Repository } from 'typeorm'; +import { Repository, EntityManager } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { v4 as uuidv4 } from 'uuid'; import { AcceptInviteDto } from './dto/accept-invite.dto'; @@ -24,7 +24,8 @@ export class InviteService { private readonly mailerService: MailerService, private readonly emailService: EmailService, private readonly configService: ConfigService, - private readonly OrganisationService: OrganisationsService + private readonly OrganisationService: OrganisationsService, + private readonly entityManager: EntityManager ) {} async getPendingInvites(): Promise<{ message: string; data: InviteDto[] }> { @@ -127,17 +128,23 @@ export class InviteService { throw new CustomHttpException(SYS_MSG.USER_NOT_REGISTERED, HttpStatus.NOT_FOUND); } - const response = await this.OrganisationService.addOrganisationMember(invite.organisation.id, { - user_id: user.id, + return await this.entityManager.transaction(async transactionalEntityManager => { + const response = await this.OrganisationService.addOrganisationMember( + invite.organisation.id, + { + user_id: user.id, + }, + transactionalEntityManager + ); + + if (response.status === 'success') { + invite.isAccepted = true; + await transactionalEntityManager.save(invite); + return response; + } else { + throw new CustomHttpException(SYS_MSG.MEMBER_NOT_ADDED, HttpStatus.INTERNAL_SERVER_ERROR); + } }); - - if (response.status === 'success') { - invite.isAccepted = true; - await this.inviteRepository.save(invite); - return response; - } else { - throw new CustomHttpException(SYS_MSG.MEMBER_NOT_ADDED, HttpStatus.INTERNAL_SERVER_ERROR); - } } async sendInvitations(createInvitationDto: CreateInvitationDto): Promise { diff --git a/src/modules/organisations/organisations.service.ts b/src/modules/organisations/organisations.service.ts index f6c187a31..f979ec29c 100644 --- a/src/modules/organisations/organisations.service.ts +++ b/src/modules/organisations/organisations.service.ts @@ -197,7 +197,7 @@ export class OrganisationsService { return userOrganisations; } - async addOrganisationMember(org_id: string, addMemberDto: AddMemberDto) { + async addOrganisationMember(org_id: string, addMemberDto: AddMemberDto, manager?: EntityManager) { const organisation = await this.organisationRepository.findOneBy({ id: org_id }); if (!organisation) { throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); @@ -227,10 +227,15 @@ export class OrganisationsService { defaultRole.organisationId = organisation.id; defaultRole.roleId = userRole.id; - await this.organisationUserRole.save(defaultRole); + const organisationUserRoleRepository = manager + ? manager.getRepository(OrganisationUserRole) + : this.organisationUserRole; + await organisationUserRoleRepository.save(defaultRole); user.organisations = [...user.organisations, organisation]; - await this.userRepository.save(user); + + const userRepository = manager ? manager.getRepository(User) : this.userRepository; + await userRepository.save(user); const responsePayload = { id: user.id, From de09e5a5d0b78dc41aa3c0888cc954525efb878d Mon Sep 17 00:00:00 2001 From: Zetro Date: Sat, 1 Mar 2025 14:28:52 +0100 Subject: [PATCH 2/2] test(invite): add EntityManager mock --- src/modules/invite/tests/invite.service.spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 491321194..bf0b02620 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -1,7 +1,7 @@ import { HttpStatus, BadRequestException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, EntityManager } from 'typeorm'; import { Organisation } from '../../organisations/entities/organisations.entity'; import { User } from '../../user/entities/user.entity'; import { Invite } from '../entities/invite.entity'; @@ -34,8 +34,14 @@ describe('InviteService', () => { let organisationService: OrganisationsService; let configService: ConfigService; let frontendUrl: string; + let entityManager: jest.Mocked; beforeEach(async () => { + entityManager = { + transaction: jest.fn().mockImplementation(async cb => cb(entityManager)), + save: jest.fn(), + } as unknown as jest.Mocked; + const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot()], providers: [ @@ -137,6 +143,10 @@ describe('InviteService', () => { sendMail: jest.fn(), }, }, + { + provide: EntityManager, + useValue: entityManager, + }, ], }).compile();