diff --git a/Dockerfile b/Dockerfile index 90f8c66..68bb1e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,12 @@ WORKDIR /usr/src/app # Copying this first prevents re-running npm install on every code change. COPY --chown=node:node package*.json ./ +# Copy prisma (needed for prisma error types) +COPY --chown=node:node ./prisma prisma + # Install app dependencies using the `npm ci` command instead of `npm install` RUN npm ci +RUN npx prisma generate # Bundle app source COPY --chown=node:node . . diff --git a/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts index c9408ca..929605a 100644 --- a/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts +++ b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts @@ -11,8 +11,8 @@ import { Configuration } from '../../domain/entities/configuration'; @Controller() export class ConfigurationMessagerController { constructor( - private readonly _commandBus: CommandBus, - private readonly _configService: ConfigService, + private readonly commandBus: CommandBus, + private readonly configService: ConfigService, ) {} @RabbitSubscribe({ @@ -22,14 +22,14 @@ export class ConfigurationMessagerController { const configuration: Configuration = JSON.parse(message); if ( configuration.domain == - this._configService.get('SERVICE_CONFIGURATION_DOMAIN') + this.configService.get('SERVICE_CONFIGURATION_DOMAIN') ) { const setConfigurationRequest: SetConfigurationRequest = new SetConfigurationRequest(); setConfigurationRequest.domain = configuration.domain; setConfigurationRequest.key = configuration.key; setConfigurationRequest.value = configuration.value; - await this._commandBus.execute( + await this.commandBus.execute( new SetConfigurationCommand(setConfigurationRequest), ); } @@ -42,12 +42,12 @@ export class ConfigurationMessagerController { const deletedConfiguration: Configuration = JSON.parse(message); if ( deletedConfiguration.domain == - this._configService.get('SERVICE_CONFIGURATION_DOMAIN') + this.configService.get('SERVICE_CONFIGURATION_DOMAIN') ) { const deleteConfigurationRequest = new DeleteConfigurationRequest(); deleteConfigurationRequest.domain = deletedConfiguration.domain; deleteConfigurationRequest.key = deletedConfiguration.key; - await this._commandBus.execute( + await this.commandBus.execute( new DeleteConfigurationCommand(deleteConfigurationRequest), ); } @@ -61,14 +61,14 @@ export class ConfigurationMessagerController { configurations.forEach(async (configuration) => { if ( configuration.domain == - this._configService.get('SERVICE_CONFIGURATION_DOMAIN') + this.configService.get('SERVICE_CONFIGURATION_DOMAIN') ) { const setConfigurationRequest: SetConfigurationRequest = new SetConfigurationRequest(); setConfigurationRequest.domain = configuration.domain; setConfigurationRequest.key = configuration.key; setConfigurationRequest.value = configuration.value; - await this._commandBus.execute( + await this.commandBus.execute( new SetConfigurationCommand(setConfigurationRequest), ); } diff --git a/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts b/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts index 2de14f0..9281a03 100644 --- a/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts +++ b/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts @@ -5,19 +5,14 @@ import { IConfigurationRepository } from '../../domain/interfaces/configuration. @Injectable() export class RedisConfigurationRepository extends IConfigurationRepository { - constructor(@InjectRedis() private readonly _redis: Redis) { + constructor(@InjectRedis() private readonly redis: Redis) { super(); } - async get(key: string): Promise { - return await this._redis.get(key); - } + get = async (key: string): Promise => await this.redis.get(key); - async set(key: string, value: string) { - await this._redis.set(key, value); - } + set = async (key: string, value: string): Promise<'OK'> => + this.redis.set(key, value); - async del(key: string) { - await this._redis.del(key); - } + del = async (key: string): Promise => this.redis.del(key); } diff --git a/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts index 14ab3cb..0ff1095 100644 --- a/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts @@ -4,13 +4,15 @@ import { DeleteConfigurationCommand } from '../../commands/delete-configuration. @CommandHandler(DeleteConfigurationCommand) export class DeleteConfigurationUseCase { - constructor(private _configurationRepository: RedisConfigurationRepository) {} + constructor(private configurationRepository: RedisConfigurationRepository) {} - async execute(deleteConfigurationCommand: DeleteConfigurationCommand) { - await this._configurationRepository.del( + execute = async ( + deleteConfigurationCommand: DeleteConfigurationCommand, + ): Promise => { + await this.configurationRepository.del( deleteConfigurationCommand.deleteConfigurationRequest.domain + ':' + deleteConfigurationCommand.deleteConfigurationRequest.key, ); - } + }; } diff --git a/src/modules/configuration/domain/usecases/get-configuration.usecase.ts b/src/modules/configuration/domain/usecases/get-configuration.usecase.ts index 38036ff..f7a5740 100644 --- a/src/modules/configuration/domain/usecases/get-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/get-configuration.usecase.ts @@ -4,11 +4,12 @@ import { GetConfigurationQuery } from '../../queries/get-configuration.query'; @QueryHandler(GetConfigurationQuery) export class GetConfigurationUseCase { - constructor(private _configurationRepository: RedisConfigurationRepository) {} + constructor(private configurationRepository: RedisConfigurationRepository) {} - async execute(getConfigurationQuery: GetConfigurationQuery): Promise { - return this._configurationRepository.get( + execute = async ( + getConfigurationQuery: GetConfigurationQuery, + ): Promise => + this.configurationRepository.get( getConfigurationQuery.domain + ':' + getConfigurationQuery.key, ); - } } diff --git a/src/modules/configuration/domain/usecases/set-configuration.usecase.ts b/src/modules/configuration/domain/usecases/set-configuration.usecase.ts index 408340a..b9da4a9 100644 --- a/src/modules/configuration/domain/usecases/set-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/set-configuration.usecase.ts @@ -6,12 +6,14 @@ import { SetConfigurationCommand } from '../../commands/set-configuration.comman export class SetConfigurationUseCase { constructor(private _configurationRepository: RedisConfigurationRepository) {} - async execute(setConfigurationCommand: SetConfigurationCommand) { + execute = async ( + setConfigurationCommand: SetConfigurationCommand, + ): Promise => { await this._configurationRepository.set( setConfigurationCommand.setConfigurationRequest.domain + ':' + setConfigurationCommand.setConfigurationRequest.key, setConfigurationCommand.setConfigurationRequest.value, ); - } + }; } diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts new file mode 100644 index 0000000..c62eaf2 --- /dev/null +++ b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts @@ -0,0 +1,259 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { DatabaseException } from '../../exceptions/database.exception'; +import { ICollection } from '../../interfaces/collection.interface'; +import { IRepository } from '../../interfaces/repository.interface'; +import { PrismaService } from './prisma-service'; + +/** + * Child classes MUST redefined _model property with appropriate model name + */ +@Injectable() +export abstract class PrismaRepository implements IRepository { + protected model: string; + + constructor(protected readonly prisma: PrismaService) {} + + findAll = async ( + page = 1, + perPage = 10, + where?: any, + include?: any, + ): Promise> => { + const [data, total] = await this.prisma.$transaction([ + this.prisma[this.model].findMany({ + where, + include, + skip: (page - 1) * perPage, + take: perPage, + }), + this.prisma[this.model].count({ + where, + }), + ]); + return Promise.resolve({ + data, + total, + }); + }; + + findOneByUuid = async (uuid: string): Promise => { + try { + const entity = await this.prisma[this.model].findUnique({ + where: { uuid }, + }); + + return entity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + findOne = async (where: any, include?: any): Promise => { + try { + const entity = await this.prisma[this.model].findFirst({ + where: where, + include: include, + }); + + return entity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + ); + } else { + throw new DatabaseException(); + } + } + }; + + // TODO : using any is not good, but needed for nested entities + // TODO : Refactor for good clean architecture ? + create = async (entity: Partial | any, include?: any): Promise => { + try { + const res = await this.prisma[this.model].create({ + data: entity, + include: include, + }); + + return res; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + update = async (uuid: string, entity: Partial): Promise => { + try { + const updatedEntity = await this.prisma[this.model].update({ + where: { uuid }, + data: entity, + }); + return updatedEntity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + updateWhere = async ( + where: any, + entity: Partial | any, + include?: any, + ): Promise => { + try { + const updatedEntity = await this.prisma[this.model].update({ + where: where, + data: entity, + include: include, + }); + + return updatedEntity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + delete = async (uuid: string): Promise => { + try { + const entity = await this.prisma[this.model].delete({ + where: { uuid }, + }); + + return entity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + deleteMany = async (where: any): Promise => { + try { + const entity = await this.prisma[this.model].deleteMany({ + where: where, + }); + + return entity; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + findAllByQuery = async ( + include: string[], + where: string[], + ): Promise> => { + const query = `SELECT ${include.join(',')} FROM ${ + this.model + } WHERE ${where.join(' AND ')}`; + const data: T[] = await this.prisma.$queryRawUnsafe(query); + return Promise.resolve({ + data, + total: data.length, + }); + }; + + createWithFields = async (fields: object): Promise => { + try { + const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join( + '","', + )}") VALUES (${Object.values(fields).join(',')})`; + return await this.prisma.$executeRawUnsafe(command); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + updateWithFields = async (uuid: string, entity: object): Promise => { + entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`; + const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`); + try { + const command = `UPDATE ${this.model} SET ${values.join( + ', ', + )} WHERE uuid = '${uuid}'`; + return await this.prisma.$executeRawUnsafe(command); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; + + healthCheck = async (): Promise => { + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + }; +} diff --git a/src/modules/database/src/adapters/secondaries/prisma-service.ts b/src/modules/database/adapters/secondaries/prisma-service.ts similarity index 100% rename from src/modules/database/src/adapters/secondaries/prisma-service.ts rename to src/modules/database/adapters/secondaries/prisma-service.ts diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index e2b09f4..82ca949 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from './src/adapters/secondaries/prisma-service'; -import { UserRepository } from './src/domain/user-repository'; +import { PrismaService } from './adapters/secondaries/prisma-service'; +import { UserRepository } from './domain/user-repository'; @Module({ providers: [PrismaService, UserRepository], diff --git a/src/modules/database/src/domain/user-repository.ts b/src/modules/database/domain/user-repository.ts similarity index 100% rename from src/modules/database/src/domain/user-repository.ts rename to src/modules/database/domain/user-repository.ts diff --git a/src/modules/database/src/exceptions/database.exception.ts b/src/modules/database/exceptions/database.exception.ts similarity index 100% rename from src/modules/database/src/exceptions/database.exception.ts rename to src/modules/database/exceptions/database.exception.ts diff --git a/src/modules/database/src/interfaces/collection.interface.ts b/src/modules/database/interfaces/collection.interface.ts similarity index 100% rename from src/modules/database/src/interfaces/collection.interface.ts rename to src/modules/database/interfaces/collection.interface.ts diff --git a/src/modules/database/src/interfaces/repository.interface.ts b/src/modules/database/interfaces/repository.interface.ts similarity index 100% rename from src/modules/database/src/interfaces/repository.interface.ts rename to src/modules/database/interfaces/repository.interface.ts diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts deleted file mode 100644 index c14086d..0000000 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; -import { DatabaseException } from '../../exceptions/database.exception'; -import { ICollection } from '../../interfaces/collection.interface'; -import { IRepository } from '../../interfaces/repository.interface'; -import { PrismaService } from './prisma-service'; - -/** - * Child classes MUST redefined _model property with appropriate model name - */ -@Injectable() -export abstract class PrismaRepository implements IRepository { - protected _model: string; - - constructor(protected readonly _prisma: PrismaService) {} - - async findAll( - page = 1, - perPage = 10, - where?: any, - include?: any, - ): Promise> { - const [data, total] = await this._prisma.$transaction([ - this._prisma[this._model].findMany({ - where, - include, - skip: (page - 1) * perPage, - take: perPage, - }), - this._prisma[this._model].count({ - where, - }), - ]); - return Promise.resolve({ - data, - total, - }); - } - - async findOneByUuid(uuid: string): Promise { - try { - const entity = await this._prisma[this._model].findUnique({ - where: { uuid }, - }); - - return entity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async findOne(where: any, include?: any): Promise { - try { - const entity = await this._prisma[this._model].findFirst({ - where: where, - include: include, - }); - - return entity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException(PrismaClientKnownRequestError.name, e.code); - } else { - throw new DatabaseException(); - } - } - } - - // TODO : using any is not good, but needed for nested entities - // TODO : Refactor for good clean architecture ? - async create(entity: Partial | any, include?: any): Promise { - try { - const res = await this._prisma[this._model].create({ - data: entity, - include: include, - }); - - return res; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async update(uuid: string, entity: Partial): Promise { - try { - const updatedEntity = await this._prisma[this._model].update({ - where: { uuid }, - data: entity, - }); - return updatedEntity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async updateWhere( - where: any, - entity: Partial | any, - include?: any, - ): Promise { - try { - const updatedEntity = await this._prisma[this._model].update({ - where: where, - data: entity, - include: include, - }); - - return updatedEntity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async delete(uuid: string): Promise { - try { - const entity = await this._prisma[this._model].delete({ - where: { uuid }, - }); - - return entity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async deleteMany(where: any): Promise { - try { - const entity = await this._prisma[this._model].deleteMany({ - where: where, - }); - - return entity; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async healthCheck(): Promise { - try { - await this._prisma.$queryRaw`SELECT 1`; - return true; - } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException( - PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } -} diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 3e2be4a..eb3bad0 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; -import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; -import { DatabaseException } from '../../src/exceptions/database.exception'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { PrismaService } from '../../adapters/secondaries/prisma-service'; +import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract'; +import { DatabaseException } from '../../exceptions/database.exception'; +import { Prisma } from '@prisma/client'; class FakeEntity { uuid?: string; @@ -41,7 +41,7 @@ Array.from({ length: 10 }).forEach(() => { @Injectable() class FakePrismaRepository extends PrismaRepository { - protected _model = 'fake'; + protected model = 'fake'; } class FakePrismaService extends PrismaService { @@ -57,10 +57,40 @@ const mockPrismaService = { return Promise.resolve([fakeEntities, fakeEntities.length]); }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + $queryRawUnsafe: jest.fn().mockImplementation((query?: string) => { + return Promise.resolve(fakeEntities); + }), + $executeRawUnsafe: jest + .fn() + .mockResolvedValueOnce(fakeEntityCreated) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new Error('an unknown error'); + }) + .mockResolvedValueOnce(fakeEntityCreated) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((fields: object) => { + throw new Error('an unknown error'); + }), $queryRaw: jest .fn() .mockImplementationOnce(() => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -69,7 +99,7 @@ const mockPrismaService = { return true; }) .mockImplementation(() => { - throw new PrismaClientKnownRequestError('Database unavailable', { + throw new Prisma.PrismaClientKnownRequestError('Database unavailable', { code: 'code', clientVersion: 'version', }); @@ -80,7 +110,7 @@ const mockPrismaService = { .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -109,7 +139,7 @@ const mockPrismaService = { } if (!entity && params?.where?.uuid == 'unknown') { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -131,7 +161,7 @@ const mockPrismaService = { }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -145,14 +175,14 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -182,7 +212,7 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -206,7 +236,7 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -440,6 +470,86 @@ describe('PrismaRepository', () => { }); }); + describe('findAllByquery', () => { + it('should return an array of entities', async () => { + const entities = await fakeRepository.findAllByQuery( + ['uuid', 'name'], + ['name is not null'], + ); + expect(entities).toStrictEqual({ + data: fakeEntities, + total: fakeEntities.length, + }); + }); + }); + + describe('createWithFields', () => { + it('should create an entity', async () => { + jest.spyOn(prisma, '$queryRawUnsafe'); + + const newEntity = await fakeRepository.createWithFields({ + uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', + name: 'my-name', + }); + expect(newEntity).toBe(fakeEntityCreated); + expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.createWithFields({ + uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', + name: 'my-name', + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.createWithFields({ + name: 'my-name', + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('updateWithFields', () => { + it('should update an entity', async () => { + jest.spyOn(prisma, '$queryRawUnsafe'); + + const updatedEntity = await fakeRepository.updateWithFields( + '804319b3-a09b-4491-9f82-7976bfce0aff', + { + name: 'my-name', + }, + ); + expect(updatedEntity).toBe(fakeEntityCreated); + expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.updateWithFields( + '804319b3-a09b-4491-9f82-7976bfce0aff', + { + name: 'my-name', + }, + ), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.updateWithFields( + '804319b3-a09b-4491-9f82-7976bfce0aff', + { + name: 'my-name', + }, + ), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + describe('healthCheck', () => { it('should throw a DatabaseException for client error', async () => { await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( diff --git a/src/modules/health/adapters/primaries/health-server.controller.ts b/src/modules/health/adapters/primaries/health-server.controller.ts index b58c761..c0d63c8 100644 --- a/src/modules/health/adapters/primaries/health-server.controller.ts +++ b/src/modules/health/adapters/primaries/health-server.controller.ts @@ -19,7 +19,7 @@ interface HealthCheckResponse { @Controller() export class HealthServerController { constructor( - private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, ) {} @GrpcMethod('Health', 'Check') @@ -29,7 +29,7 @@ export class HealthServerController { // eslint-disable-next-line @typescript-eslint/no-unused-vars metadata: any, ): Promise { - const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy( + const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy( 'prisma', ); return { diff --git a/src/modules/health/adapters/primaries/health.controller.ts b/src/modules/health/adapters/primaries/health.controller.ts index ee45f63..42a9cc6 100644 --- a/src/modules/health/adapters/primaries/health.controller.ts +++ b/src/modules/health/adapters/primaries/health.controller.ts @@ -10,21 +10,21 @@ import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.healt @Controller('health') export class HealthController { constructor( - private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, - private _healthCheckService: HealthCheckService, - private _messager: Messager, + private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + private readonly healthCheckService: HealthCheckService, + private readonly messager: Messager, ) {} @Get() @HealthCheck() async check() { try { - return await this._healthCheckService.check([ - async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'), + return await this.healthCheckService.check([ + async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'), ]); } catch (error) { const healthCheckResult: HealthCheckResult = error.response; - this._messager.publish( + this.messager.publish( 'logging.user.health.crit', JSON.stringify(healthCheckResult.error), ); diff --git a/src/modules/health/adapters/secondaries/messager.ts b/src/modules/health/adapters/secondaries/messager.ts index 0725261..567a23f 100644 --- a/src/modules/health/adapters/secondaries/messager.ts +++ b/src/modules/health/adapters/secondaries/messager.ts @@ -6,13 +6,13 @@ import { IMessageBroker } from './message-broker'; @Injectable() export class Messager extends IMessageBroker { constructor( - private readonly _amqpConnection: AmqpConnection, - configService: ConfigService, + private readonly amqpConnection: AmqpConnection, + private readonly configService: ConfigService, ) { super(configService.get('RMQ_EXCHANGE')); } - publish(routingKey: string, message: string): void { - this._amqpConnection.publish(this.exchange, routingKey, message); - } + publish = (routingKey: string, message: string): void => { + this.amqpConnection.publish(this.exchange, routingKey, message); + }; } diff --git a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts index 464e2c0..e30a617 100644 --- a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts +++ b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts @@ -8,18 +8,18 @@ import { UsersRepository } from '../../../user/adapters/secondaries/users.reposi @Injectable() export class PrismaHealthIndicatorUseCase extends HealthIndicator { - constructor(private readonly _repository: UsersRepository) { + constructor(private readonly repository: UsersRepository) { super(); } - async isHealthy(key: string): Promise { + isHealthy = async (key: string): Promise => { try { - await this._repository.healthCheck(); + await this.repository.healthCheck(); return this.getStatus(key, true); } catch (e) { throw new HealthCheckError('Prisma', { prisma: e.message, }); } - } + }; } diff --git a/src/modules/user/adapters/primaries/user.controller.ts b/src/modules/user/adapters/primaries/user.controller.ts index dcbfee7..5d6ca3b 100644 --- a/src/modules/user/adapters/primaries/user.controller.ts +++ b/src/modules/user/adapters/primaries/user.controller.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { DatabaseException } from '../../../database/src/exceptions/database.exception'; +import { DatabaseException } from '../../../database/exceptions/database.exception'; import { CreateUserCommand } from '../../commands/create-user.command'; import { DeleteUserCommand } from '../../commands/delete-user.command'; import { UpdateUserCommand } from '../../commands/update-user.command'; @@ -21,7 +21,7 @@ import { User } from '../../domain/entities/user'; import { FindAllUsersQuery } from '../../queries/find-all-users.query'; import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query'; import { UserPresenter } from './user.presenter'; -import { ICollection } from '../../../database/src/interfaces/collection.interface'; +import { ICollection } from '../../../database/interfaces/collection.interface'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; @UsePipes( @@ -33,21 +33,21 @@ import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; @Controller() export class UserController { constructor( - private readonly _commandBus: CommandBus, - private readonly _queryBus: QueryBus, - @InjectMapper() private readonly _mapper: Mapper, + private readonly commandBus: CommandBus, + private readonly queryBus: QueryBus, + @InjectMapper() private readonly mapper: Mapper, ) {} @GrpcMethod('UsersService', 'FindAll') @UseInterceptors(CacheInterceptor) @CacheKey('UsersServiceFindAll') async findAll(data: FindAllUsersRequest): Promise> { - const userCollection = await this._queryBus.execute( + const userCollection = await this.queryBus.execute( new FindAllUsersQuery(data), ); return Promise.resolve({ data: userCollection.data.map((user: User) => - this._mapper.map(user, User, UserPresenter), + this.mapper.map(user, User, UserPresenter), ), total: userCollection.total, }); @@ -58,8 +58,8 @@ export class UserController { @CacheKey('UsersServiceFindOneByUuid') async findOneByUuid(data: FindUserByUuidRequest): Promise { try { - const user = await this._queryBus.execute(new FindUserByUuidQuery(data)); - return this._mapper.map(user, User, UserPresenter); + const user = await this.queryBus.execute(new FindUserByUuidQuery(data)); + return this.mapper.map(user, User, UserPresenter); } catch (error) { throw new RpcException({ code: 5, @@ -71,8 +71,8 @@ export class UserController { @GrpcMethod('UsersService', 'Create') async createUser(data: CreateUserRequest): Promise { try { - const user = await this._commandBus.execute(new CreateUserCommand(data)); - return this._mapper.map(user, User, UserPresenter); + const user = await this.commandBus.execute(new CreateUserCommand(data)); + return this.mapper.map(user, User, UserPresenter); } catch (e) { if (e instanceof DatabaseException) { if (e.message.includes('Already exists')) { @@ -89,9 +89,9 @@ export class UserController { @GrpcMethod('UsersService', 'Update') async updateUser(data: UpdateUserRequest): Promise { try { - const user = await this._commandBus.execute(new UpdateUserCommand(data)); + const user = await this.commandBus.execute(new UpdateUserCommand(data)); - return this._mapper.map(user, User, UserPresenter); + return this.mapper.map(user, User, UserPresenter); } catch (e) { if (e instanceof DatabaseException) { if (e.message.includes('not found')) { @@ -108,7 +108,7 @@ export class UserController { @GrpcMethod('UsersService', 'Delete') async deleteUser(data: FindUserByUuidRequest): Promise { try { - await this._commandBus.execute(new DeleteUserCommand(data.uuid)); + await this.commandBus.execute(new DeleteUserCommand(data.uuid)); return Promise.resolve(); } catch (e) { diff --git a/src/modules/user/adapters/secondaries/messager.ts b/src/modules/user/adapters/secondaries/messager.ts index 0ee32e3..d8fdac1 100644 --- a/src/modules/user/adapters/secondaries/messager.ts +++ b/src/modules/user/adapters/secondaries/messager.ts @@ -6,13 +6,13 @@ import { IMessageBroker } from '../../domain/interfaces/message-broker'; @Injectable() export class Messager extends IMessageBroker { constructor( - private readonly _amqpConnection: AmqpConnection, - configService: ConfigService, + private readonly amqpConnection: AmqpConnection, + private readonly configService: ConfigService, ) { super(configService.get('RMQ_EXCHANGE')); } - publish(routingKey: string, message: string): void { - this._amqpConnection.publish(this.exchange, routingKey, message); - } + publish = (routingKey: string, message: string): void => { + this.amqpConnection.publish(this.exchange, routingKey, message); + }; } diff --git a/src/modules/user/adapters/secondaries/users.repository.ts b/src/modules/user/adapters/secondaries/users.repository.ts index 927e17c..72d047f 100644 --- a/src/modules/user/adapters/secondaries/users.repository.ts +++ b/src/modules/user/adapters/secondaries/users.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { UserRepository } from '../../../database/src/domain/user-repository'; +import { UserRepository } from '../../../database/domain/user-repository'; import { User } from '../../domain/entities/user'; @Injectable() export class UsersRepository extends UserRepository { - protected _model = 'user'; + protected model = 'user'; } diff --git a/src/modules/user/domain/usecases/create-user.usecase.ts b/src/modules/user/domain/usecases/create-user.usecase.ts index 25771d4..46daa1a 100644 --- a/src/modules/user/domain/usecases/create-user.usecase.ts +++ b/src/modules/user/domain/usecases/create-user.usecase.ts @@ -10,29 +10,29 @@ import { User } from '../entities/user'; @CommandHandler(CreateUserCommand) export class CreateUserUseCase { constructor( - private readonly _repository: UsersRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: UsersRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - async execute(command: CreateUserCommand): Promise { - const entity = this._mapper.map( + execute = async (command: CreateUserCommand): Promise => { + const entity = this.mapper.map( command.createUserRequest, CreateUserRequest, User, ); try { - const user = await this._repository.create(entity); - this._messager.publish('user.create', JSON.stringify(user)); - this._messager.publish('logging.user.create.info', JSON.stringify(user)); + const user = await this.repository.create(entity); + this.messager.publish('user.create', JSON.stringify(user)); + this.messager.publish('logging.user.create.info', JSON.stringify(user)); return user; } catch (error) { let key = 'logging.user.create.crit'; if (error.message.includes('Already exists')) { key = 'logging.user.create.warning'; } - this._messager.publish( + this.messager.publish( key, JSON.stringify({ command, @@ -41,5 +41,5 @@ export class CreateUserUseCase { ); throw error; } - } + }; } diff --git a/src/modules/user/domain/usecases/delete-user.usecase.ts b/src/modules/user/domain/usecases/delete-user.usecase.ts index 1b575cd..251b7b5 100644 --- a/src/modules/user/domain/usecases/delete-user.usecase.ts +++ b/src/modules/user/domain/usecases/delete-user.usecase.ts @@ -7,24 +7,21 @@ import { User } from '../entities/user'; @CommandHandler(DeleteUserCommand) export class DeleteUserUseCase { constructor( - private readonly _repository: UsersRepository, - private readonly _messager: Messager, + private readonly repository: UsersRepository, + private readonly messager: Messager, ) {} - async execute(command: DeleteUserCommand): Promise { + execute = async (command: DeleteUserCommand): Promise => { try { - const user = await this._repository.delete(command.uuid); - this._messager.publish( - 'user.delete', - JSON.stringify({ uuid: user.uuid }), - ); - this._messager.publish( + const user = await this.repository.delete(command.uuid); + this.messager.publish('user.delete', JSON.stringify({ uuid: user.uuid })); + this.messager.publish( 'logging.user.delete.info', JSON.stringify({ uuid: user.uuid }), ); return user; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.user.delete.crit', JSON.stringify({ command, @@ -33,5 +30,5 @@ export class DeleteUserUseCase { ); throw error; } - } + }; } diff --git a/src/modules/user/domain/usecases/find-all-users.usecase.ts b/src/modules/user/domain/usecases/find-all-users.usecase.ts index 971dde2..99b18b1 100644 --- a/src/modules/user/domain/usecases/find-all-users.usecase.ts +++ b/src/modules/user/domain/usecases/find-all-users.usecase.ts @@ -1,19 +1,15 @@ import { QueryHandler } from '@nestjs/cqrs'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { ICollection } from 'src/modules/database/interfaces/collection.interface'; import { UsersRepository } from '../../adapters/secondaries/users.repository'; import { FindAllUsersQuery } from '../../queries/find-all-users.query'; import { User } from '../entities/user'; @QueryHandler(FindAllUsersQuery) export class FindAllUsersUseCase { - constructor(private readonly _repository: UsersRepository) {} + constructor(private readonly repository: UsersRepository) {} - async execute( + execute = async ( findAllUsersQuery: FindAllUsersQuery, - ): Promise> { - return this._repository.findAll( - findAllUsersQuery.page, - findAllUsersQuery.perPage, - ); - } + ): Promise> => + this.repository.findAll(findAllUsersQuery.page, findAllUsersQuery.perPage); } diff --git a/src/modules/user/domain/usecases/find-user-by-uuid.usecase.ts b/src/modules/user/domain/usecases/find-user-by-uuid.usecase.ts index 8c5d3d1..31dafbb 100644 --- a/src/modules/user/domain/usecases/find-user-by-uuid.usecase.ts +++ b/src/modules/user/domain/usecases/find-user-by-uuid.usecase.ts @@ -8,17 +8,17 @@ import { User } from '../entities/user'; @QueryHandler(FindUserByUuidQuery) export class FindUserByUuidUseCase { constructor( - private readonly _repository: UsersRepository, - private readonly _messager: Messager, + private readonly repository: UsersRepository, + private readonly messager: Messager, ) {} - async execute(findUserByUuid: FindUserByUuidQuery): Promise { + execute = async (findUserByUuid: FindUserByUuidQuery): Promise => { try { - const user = await this._repository.findOneByUuid(findUserByUuid.uuid); + const user = await this.repository.findOneByUuid(findUserByUuid.uuid); if (!user) throw new NotFoundException(); return user; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.user.read.warning', JSON.stringify({ query: findUserByUuid, @@ -27,5 +27,5 @@ export class FindUserByUuidUseCase { ); throw error; } - } + }; } diff --git a/src/modules/user/domain/usecases/update-user.usecase.ts b/src/modules/user/domain/usecases/update-user.usecase.ts index e7e370b..1530bfa 100644 --- a/src/modules/user/domain/usecases/update-user.usecase.ts +++ b/src/modules/user/domain/usecases/update-user.usecase.ts @@ -10,34 +10,34 @@ import { User } from '../entities/user'; @CommandHandler(UpdateUserCommand) export class UpdateUserUseCase { constructor( - private readonly _repository: UsersRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: UsersRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - async execute(command: UpdateUserCommand): Promise { - const entity = this._mapper.map( + execute = async (command: UpdateUserCommand): Promise => { + const entity = this.mapper.map( command.updateUserRequest, UpdateUserRequest, User, ); try { - const user = await this._repository.update( + const user = await this.repository.update( command.updateUserRequest.uuid, entity, ); - this._messager.publish( + this.messager.publish( 'user.update', JSON.stringify(command.updateUserRequest), ); - this._messager.publish( + this.messager.publish( 'logging.user.update.info', JSON.stringify(command.updateUserRequest), ); return user; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.user.update.crit', JSON.stringify({ command, @@ -46,5 +46,5 @@ export class UpdateUserUseCase { ); throw error; } - } + }; } diff --git a/src/modules/user/tests/integration/users.repository.spec.ts b/src/modules/user/tests/integration/users.repository.spec.ts index e71c6cc..e185654 100644 --- a/src/modules/user/tests/integration/users.repository.spec.ts +++ b/src/modules/user/tests/integration/users.repository.spec.ts @@ -1,7 +1,7 @@ import { TestingModule, Test } from '@nestjs/testing'; import { DatabaseModule } from '../../../database/database.module'; -import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; -import { DatabaseException } from '../../../database/src/exceptions/database.exception'; +import { PrismaService } from '../../../database/adapters/secondaries/prisma-service'; +import { DatabaseException } from '../../../database/exceptions/database.exception'; import { UsersRepository } from '../../adapters/secondaries/users.repository'; import { User } from '../../domain/entities/user';