diff --git a/src/app.module.ts b/src/app.module.ts index 0eb178f..4fcd719 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { WalletModule } from '@modules/wallet/wallet.module'; import { GraphQLModule } from '@nestjs/graphql'; import { join } from 'path'; import { typeormConfig } from './infrastructure/configs/ormconfig'; +import { UnitOfWorkModule } from './infrastructure/database/unit-of-work/unit-of-work.module'; @Module({ imports: [ @@ -15,6 +16,7 @@ import { typeormConfig } from './infrastructure/configs/ormconfig'; GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/infrastructure/schema.gql'), }), + UnitOfWorkModule, NestEventModule, ConsoleModule, UserModule, diff --git a/src/infrastructure/database/unit-of-work/unit-of-work.module.ts b/src/infrastructure/database/unit-of-work/unit-of-work.module.ts new file mode 100644 index 0000000..a035712 --- /dev/null +++ b/src/infrastructure/database/unit-of-work/unit-of-work.module.ts @@ -0,0 +1,17 @@ +import { Global, Module } from '@nestjs/common'; +import { UnitOfWork } from './unit-of-work'; + +const unitOfWorkSingleton = new UnitOfWork(); + +const unitOfWorkSingletonProvider = { + provide: UnitOfWork, + useFactory: () => unitOfWorkSingleton, +}; + +@Global() +@Module({ + imports: [], + providers: [unitOfWorkSingletonProvider], + exports: [UnitOfWork], +}) +export class UnitOfWorkModule {} diff --git a/src/infrastructure/database/unit-of-work.ts b/src/infrastructure/database/unit-of-work/unit-of-work.ts similarity index 82% rename from src/infrastructure/database/unit-of-work.ts rename to src/infrastructure/database/unit-of-work/unit-of-work.ts index 110f229..608b94d 100644 --- a/src/infrastructure/database/unit-of-work.ts +++ b/src/infrastructure/database/unit-of-work/unit-of-work.ts @@ -3,20 +3,22 @@ import { UserOrmEntity } from '@modules/user/database/user.orm-entity'; import { UserRepository } from '@modules/user/database/user.repository'; import { WalletOrmEntity } from '@modules/wallet/database/wallet.orm-entity'; import { WalletRepository } from '@modules/wallet/database/wallet.repository'; +import { Injectable } from '@nestjs/common'; +@Injectable() export class UnitOfWork extends TypeormUnitOfWork { // Add new repositories below to use this generic UnitOfWork // Convert TypeOrm Repository to a Domain Repository getUserRepository(correlationId: string): UserRepository { return new UserRepository( - UnitOfWork.getOrmRepository(UserOrmEntity, correlationId), + this.getOrmRepository(UserOrmEntity, correlationId), ).setCorrelationId(correlationId); } getWalletRepository(correlationId: string): WalletRepository { return new WalletRepository( - UnitOfWork.getOrmRepository(WalletOrmEntity, correlationId), + this.getOrmRepository(WalletOrmEntity, correlationId), ).setCorrelationId(correlationId); } } diff --git a/src/libs/ddd/domain/base-classes/command-handler.base.ts b/src/libs/ddd/domain/base-classes/command-handler.base.ts index 8901a86..536a3f9 100644 --- a/src/libs/ddd/domain/base-classes/command-handler.base.ts +++ b/src/libs/ddd/domain/base-classes/command-handler.base.ts @@ -1,3 +1,4 @@ +import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; import { UnitOfWorkPort } from '../ports/unit-of-work.port'; import { ID } from '../value-objects/id.value-object'; import { Command } from './command.base'; @@ -7,10 +8,15 @@ export abstract class CommandHandler { protected abstract execute(command: Command): Promise; - async executeUnitOfWork(command: Command): Promise { + async executeUnitOfWork( + command: Command, + options?: { isolationLevel: IsolationLevel }, + ): Promise { this.unitOfWork.init(command.correlationId); - return this.unitOfWork.execute(command.correlationId, async () => - this.execute(command), + return this.unitOfWork.execute( + command.correlationId, + async () => this.execute(command), + options, ); } } diff --git a/src/libs/ddd/domain/ports/unit-of-work.port.ts b/src/libs/ddd/domain/ports/unit-of-work.port.ts index a2c81ee..b5a0333 100644 --- a/src/libs/ddd/domain/ports/unit-of-work.port.ts +++ b/src/libs/ddd/domain/ports/unit-of-work.port.ts @@ -1,4 +1,8 @@ export interface UnitOfWorkPort { init(correlationId: string): void; - execute(correlationId: string, callback: () => Promise): Promise; + execute( + correlationId: string, + callback: () => Promise, + options?: unknown, + ): Promise; } diff --git a/src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts b/src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts index d57ffcc..9cdf57a 100644 --- a/src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts +++ b/src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts @@ -1,22 +1,15 @@ import { Logger } from '@nestjs/common'; import { UnitOfWorkPort } from '@src/libs/ddd/domain/ports/unit-of-work.port'; import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm'; +import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; export class TypeormUnitOfWork implements UnitOfWorkPort { - init(correlationId: string): void { - return TypeormUnitOfWork.init(correlationId); - } - - execute(correlationId: string, callback: () => Promise): Promise { - return TypeormUnitOfWork.execute(correlationId, callback); - } - - private static queryRunners: Map = new Map(); + private queryRunners: Map = new Map(); /** * Creates a connection pool with a specified ID. */ - static init(correlationId: string): void { + init(correlationId: string): void { if (!correlationId) { throw new Error('Correlation ID must be provided'); } @@ -24,7 +17,7 @@ export class TypeormUnitOfWork implements UnitOfWorkPort { this.queryRunners.set(correlationId, queryRunner); } - static getQueryRunner(correlationId: string): QueryRunner { + getQueryRunner(correlationId: string): QueryRunner { const queryRunner = this.queryRunners.get(correlationId); if (!queryRunner) { throw new Error( @@ -34,7 +27,7 @@ export class TypeormUnitOfWork implements UnitOfWorkPort { return queryRunner; } - static getOrmRepository( + getOrmRepository( entity: EntityTarget, correlationId: string, ): Repository { @@ -46,16 +39,16 @@ export class TypeormUnitOfWork implements UnitOfWorkPort { * Execute a UnitOfWork. * Database operations wrapped in a UnitOfWork will execute in a single * transactional operation, so everything gets saved or nothing. - * Make sure to generate and inject correct repositories for this to work. */ - static async execute( + async execute( correlationId: string, callback: () => Promise, + options?: { isolationLevel: IsolationLevel }, ): Promise { - const logger = new Logger(`${this.name}:${correlationId}`); + const logger = new Logger(`${this.constructor.name}:${correlationId}`); logger.debug(`[Starting transaction]`); const queryRunner = this.getQueryRunner(correlationId); - await queryRunner.startTransaction('SERIALIZABLE'); + await queryRunner.startTransaction(options?.isolationLevel); let result: T; try { result = await callback(); @@ -79,7 +72,7 @@ export class TypeormUnitOfWork implements UnitOfWorkPort { return result; } - private static async finish(correlationId: string): Promise { + private async finish(correlationId: string): Promise { const queryRunner = this.getQueryRunner(correlationId); try { await queryRunner.release(); diff --git a/src/modules/user/commands/create-user/create-user.service.ts b/src/modules/user/commands/create-user/create-user.service.ts index d94c2e4..4f94697 100644 --- a/src/modules/user/commands/create-user/create-user.service.ts +++ b/src/modules/user/commands/create-user/create-user.service.ts @@ -3,7 +3,7 @@ import { UserRepositoryPort } from '@modules/user/database/user.repository.port' import { ConflictException } from '@libs/exceptions'; import { Address } from '@modules/user/domain/value-objects/address.value-object'; import { Email } from '@modules/user/domain/value-objects/email.value-object'; -import { UnitOfWork } from '@src/infrastructure/database/unit-of-work'; +import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work'; import { CommandHandler } from '@src/libs/ddd/domain/base-classes/command-handler.base'; import { CreateUserCommand } from './create-user.command'; import { UserEntity } from '../../domain/entities/user.entity'; diff --git a/src/modules/user/user.providers.ts b/src/modules/user/user.providers.ts index 1a1a9f4..00538b7 100644 --- a/src/modules/user/user.providers.ts +++ b/src/modules/user/user.providers.ts @@ -1,5 +1,5 @@ import { Logger, Provider } from '@nestjs/common'; -import { UnitOfWork } from '@src/infrastructure/database/unit-of-work'; +import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work'; import { UserRepository } from './database/user.repository'; import { CreateUserService } from './commands/create-user/create-user.service'; import { DeleteUserService } from './commands/delete-user/delete-user.service'; @@ -16,10 +16,10 @@ export const createUserSymbol = Symbol('createUser'); export const createUserProvider: Provider = { provide: createUserSymbol, - useFactory: (): CreateUserService => { - return new CreateUserService(new UnitOfWork()); + useFactory: (unitOfWork: UnitOfWork): CreateUserService => { + return new CreateUserService(unitOfWork); }, - inject: [UserRepository], + inject: [UnitOfWork], }; export const removeUserSymbol = Symbol('removeUser'); diff --git a/src/modules/wallet/application/event-handlers/create-wallet-when-user-is-created.domain-event-handler.ts b/src/modules/wallet/application/event-handlers/create-wallet-when-user-is-created.domain-event-handler.ts index d3ee4c8..de49c4c 100644 --- a/src/modules/wallet/application/event-handlers/create-wallet-when-user-is-created.domain-event-handler.ts +++ b/src/modules/wallet/application/event-handlers/create-wallet-when-user-is-created.domain-event-handler.ts @@ -2,7 +2,7 @@ import { UserCreatedDomainEvent } from '@modules/user/domain/events/user-created import { WalletRepositoryPort } from '@modules/wallet/database/wallet.repository.port'; import { DomainEventHandler } from '@libs/ddd/domain/domain-events'; import { UUID } from '@libs/ddd/domain/value-objects/uuid.value-object'; -import { UnitOfWork } from '@src/infrastructure/database/unit-of-work'; +import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work'; import { WalletEntity } from '../../domain/entities/wallet.entity'; export class CreateWalletWhenUserIsCreatedDomainEventHandler extends DomainEventHandler { diff --git a/src/modules/wallet/wallet.providers.ts b/src/modules/wallet/wallet.providers.ts index e979b90..8fa6630 100644 --- a/src/modules/wallet/wallet.providers.ts +++ b/src/modules/wallet/wallet.providers.ts @@ -1,14 +1,17 @@ import { Provider } from '@nestjs/common'; -import { UnitOfWork } from '@src/infrastructure/database/unit-of-work'; +import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work'; import { CreateWalletWhenUserIsCreatedDomainEventHandler } from './application/event-handlers/create-wallet-when-user-is-created.domain-event-handler'; export const createWalletWhenUserIsCreatedProvider: Provider = { provide: CreateWalletWhenUserIsCreatedDomainEventHandler, - useFactory: (): CreateWalletWhenUserIsCreatedDomainEventHandler => { + useFactory: ( + unitOfWork: UnitOfWork, + ): CreateWalletWhenUserIsCreatedDomainEventHandler => { const eventHandler = new CreateWalletWhenUserIsCreatedDomainEventHandler( - new UnitOfWork(), + unitOfWork, ); eventHandler.listen(); return eventHandler; }, + inject: [UnitOfWork], }; diff --git a/tests/user/create-user/create-user.artillery.yaml b/tests/user/create-user/create-user.artillery.yaml index 6c71d72..9d69aca 100644 --- a/tests/user/create-user/create-user.artillery.yaml +++ b/tests/user/create-user/create-user.artillery.yaml @@ -5,8 +5,8 @@ config: target: http://localhost:3000/v1 phases: - - duration: 1 - arrivalRate: 50 + - duration: 2 + arrivalRate: 150 plugins: faker: locale: en