diff --git a/Dockerfile b/Dockerfile index 51922ba..59c44e3 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.controller.ts b/src/modules/configuration/adapters/primaries/configuration.controller.ts index f3cc6a0..9f2627d 100644 --- a/src/modules/configuration/adapters/primaries/configuration.controller.ts +++ b/src/modules/configuration/adapters/primaries/configuration.controller.ts @@ -3,8 +3,8 @@ import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { DatabaseException } from 'src/modules/database/src/exceptions/database.exception'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { DatabaseException } from 'src/modules/database/exceptions/database.exception'; +import { ICollection } from 'src/modules/database/interfaces/collection.interface'; import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe'; import { CreateConfigurationCommand } from '../../commands/create-configuration.command'; import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; @@ -28,21 +28,21 @@ import { ConfigurationPresenter } from './configuration.presenter'; @Controller() export class ConfigurationController { 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('ConfigurationService', 'FindAll') async findAll( data: FindAllConfigurationsRequest, ): Promise> { - const configurationCollection = await this._queryBus.execute( + const configurationCollection = await this.queryBus.execute( new FindAllConfigurationsQuery(data), ); return Promise.resolve({ data: configurationCollection.data.map((configuration: Configuration) => - this._mapper.map(configuration, Configuration, ConfigurationPresenter), + this.mapper.map(configuration, Configuration, ConfigurationPresenter), ), total: configurationCollection.total, }); @@ -53,10 +53,10 @@ export class ConfigurationController { data: FindConfigurationByUuidRequest, ): Promise { try { - const configuration = await this._queryBus.execute( + const configuration = await this.queryBus.execute( new FindConfigurationByUuidQuery(data), ); - return this._mapper.map( + return this.mapper.map( configuration, Configuration, ConfigurationPresenter, @@ -74,10 +74,10 @@ export class ConfigurationController { data: CreateConfigurationRequest, ): Promise { try { - const configuration = await this._commandBus.execute( + const configuration = await this.commandBus.execute( new CreateConfigurationCommand(data), ); - return this._mapper.map( + return this.mapper.map( configuration, Configuration, ConfigurationPresenter, @@ -100,11 +100,11 @@ export class ConfigurationController { data: UpdateConfigurationRequest, ): Promise { try { - const configuration = await this._commandBus.execute( + const configuration = await this.commandBus.execute( new UpdateConfigurationCommand(data), ); - return this._mapper.map( + return this.mapper.map( configuration, Configuration, ConfigurationPresenter, @@ -127,7 +127,7 @@ export class ConfigurationController { data: FindConfigurationByUuidRequest, ): Promise { try { - await this._commandBus.execute(new DeleteConfigurationCommand(data.uuid)); + await this.commandBus.execute(new DeleteConfigurationCommand(data.uuid)); return Promise.resolve(); } catch (e) { @@ -146,7 +146,7 @@ export class ConfigurationController { @GrpcMethod('ConfigurationService', 'Propagate') async propagate(): Promise { try { - await this._queryBus.execute(new PropagateConfigurationsQuery()); + await this.queryBus.execute(new PropagateConfigurationsQuery()); } catch (e) { throw new RpcException({}); } diff --git a/src/modules/configuration/adapters/secondaries/configuration.repository.ts b/src/modules/configuration/adapters/secondaries/configuration.repository.ts index 5259497..2a1a448 100644 --- a/src/modules/configuration/adapters/secondaries/configuration.repository.ts +++ b/src/modules/configuration/adapters/secondaries/configuration.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ConfigRepository } from '../../../database/src/domain/configuration.repository'; +import { ConfigRepository } from '../../../database/domain/configuration.repository'; import { Configuration } from '../../domain/entities/configuration'; @Injectable() export class ConfigurationRepository extends ConfigRepository { - protected _model = 'configuration'; + protected model = 'configuration'; } diff --git a/src/modules/configuration/adapters/secondaries/messager.ts b/src/modules/configuration/adapters/secondaries/messager.ts index 0ee32e3..d8fdac1 100644 --- a/src/modules/configuration/adapters/secondaries/messager.ts +++ b/src/modules/configuration/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/configuration/domain/usecases/create-configuration.usecase.ts b/src/modules/configuration/domain/usecases/create-configuration.usecase.ts index 7b54238..7c67ae9 100644 --- a/src/modules/configuration/domain/usecases/create-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/create-configuration.usecase.ts @@ -11,31 +11,33 @@ import { Configuration } from '../entities/configuration'; @CommandHandler(CreateConfigurationCommand) export class CreateConfigurationUseCase { constructor( - private readonly _repository: ConfigurationRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: ConfigurationRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - async execute(command: CreateConfigurationCommand): Promise { - const entity = this._mapper.map( + execute = async ( + command: CreateConfigurationCommand, + ): Promise => { + const entity = this.mapper.map( command.createConfigurationRequest, CreateConfigurationRequest, Configuration, ); try { - const configuration = await this._repository.create(entity); - this._messager.publish( + const configuration = await this.repository.create(entity); + this.messager.publish( 'configuration.create', JSON.stringify( - this._mapper.map( + this.mapper.map( configuration, Configuration, ConfigurationMessagerPresenter, ), ), ); - this._messager.publish( + this.messager.publish( 'logging.configuration.create.info', JSON.stringify(configuration), ); @@ -45,7 +47,7 @@ export class CreateConfigurationUseCase { if (error.message.includes('Already exists')) { key = 'logging.configuration.create.warning'; } - this._messager.publish( + this.messager.publish( key, JSON.stringify({ command, @@ -54,5 +56,5 @@ export class CreateConfigurationUseCase { ); throw error; } - } + }; } diff --git a/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts index f9f4b45..d8aee1e 100644 --- a/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts @@ -10,31 +10,33 @@ import { Configuration } from '../entities/configuration'; @CommandHandler(DeleteConfigurationCommand) export class DeleteConfigurationUseCase { constructor( - private readonly _repository: ConfigurationRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: ConfigurationRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - async execute(command: DeleteConfigurationCommand): Promise { + execute = async ( + command: DeleteConfigurationCommand, + ): Promise => { try { - const configuration = await this._repository.delete(command.uuid); - this._messager.publish( + const configuration = await this.repository.delete(command.uuid); + this.messager.publish( 'configuration.delete', JSON.stringify( - this._mapper.map( + this.mapper.map( configuration, Configuration, ConfigurationMessagerPresenter, ), ), ); - this._messager.publish( + this.messager.publish( 'logging.configuration.delete.info', JSON.stringify({ uuid: configuration.uuid }), ); return configuration; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.configuration.delete.crit', JSON.stringify({ command, @@ -43,5 +45,5 @@ export class DeleteConfigurationUseCase { ); throw error; } - } + }; } diff --git a/src/modules/configuration/domain/usecases/find-all-configurations.usecase.ts b/src/modules/configuration/domain/usecases/find-all-configurations.usecase.ts index 84ebd31..98be9bc 100644 --- a/src/modules/configuration/domain/usecases/find-all-configurations.usecase.ts +++ b/src/modules/configuration/domain/usecases/find-all-configurations.usecase.ts @@ -1,19 +1,18 @@ 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 { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query'; import { Configuration } from '../entities/configuration'; @QueryHandler(FindAllConfigurationsQuery) export class FindAllConfigurationsUseCase { - constructor(private readonly _repository: ConfigurationRepository) {} + constructor(private readonly repository: ConfigurationRepository) {} - async execute( + execute = async ( findAllConfigurationsQuery: FindAllConfigurationsQuery, - ): Promise> { - return this._repository.findAll( + ): Promise> => + this.repository.findAll( findAllConfigurationsQuery.page, findAllConfigurationsQuery.perPage, ); - } } diff --git a/src/modules/configuration/domain/usecases/find-configuration-by-uuid.usecase.ts b/src/modules/configuration/domain/usecases/find-configuration-by-uuid.usecase.ts index 6d882e9..d9ee69f 100644 --- a/src/modules/configuration/domain/usecases/find-configuration-by-uuid.usecase.ts +++ b/src/modules/configuration/domain/usecases/find-configuration-by-uuid.usecase.ts @@ -8,21 +8,21 @@ import { Configuration } from '../entities/configuration'; @QueryHandler(FindConfigurationByUuidQuery) export class FindConfigurationByUuidUseCase { constructor( - private readonly _repository: ConfigurationRepository, - private readonly _messager: Messager, + private readonly repository: ConfigurationRepository, + private readonly messager: Messager, ) {} - async execute( + execute = async ( findConfigurationByUuid: FindConfigurationByUuidQuery, - ): Promise { + ): Promise => { try { - const configuration = await this._repository.findOneByUuid( + const configuration = await this.repository.findOneByUuid( findConfigurationByUuid.uuid, ); if (!configuration) throw new NotFoundException(); return configuration; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.configuration.read.warning', JSON.stringify({ query: findConfigurationByUuid, @@ -31,5 +31,5 @@ export class FindConfigurationByUuidUseCase { ); throw error; } - } + }; } diff --git a/src/modules/configuration/domain/usecases/propagate-configurations.usecase.ts b/src/modules/configuration/domain/usecases/propagate-configurations.usecase.ts index d313e54..5698e3e 100644 --- a/src/modules/configuration/domain/usecases/propagate-configurations.usecase.ts +++ b/src/modules/configuration/domain/usecases/propagate-configurations.usecase.ts @@ -10,20 +10,22 @@ import { Configuration } from '../entities/configuration'; @QueryHandler(PropagateConfigurationsQuery) export class PropagateConfigurationsUseCase { constructor( - private readonly _repository: ConfigurationRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: ConfigurationRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async execute(propagateConfigurationsQuery: PropagateConfigurationsQuery) { + execute = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + propagateConfigurationsQuery: PropagateConfigurationsQuery, + ) => { try { - const configurations = await this._repository.findAll(1, 999999); - this._messager.publish( + const configurations = await this.repository.findAll(1, 999999); + this.messager.publish( 'configuration.propagate', JSON.stringify( configurations.data.map((configuration) => - this._mapper.map( + this.mapper.map( configuration, Configuration, ConfigurationMessagerPresenter, @@ -31,16 +33,10 @@ export class PropagateConfigurationsUseCase { ), ), ); - this._messager.publish( - 'logging.configuration.update.info', - 'propagation', - ); + this.messager.publish('logging.configuration.update.info', 'propagation'); } catch (error) { - this._messager.publish( - 'logging.configuration.update.crit', - 'propagation', - ); + this.messager.publish('logging.configuration.update.crit', 'propagation'); throw error; } - } + }; } diff --git a/src/modules/configuration/domain/usecases/update-configuration.usecase.ts b/src/modules/configuration/domain/usecases/update-configuration.usecase.ts index dfa171d..6e8872a 100644 --- a/src/modules/configuration/domain/usecases/update-configuration.usecase.ts +++ b/src/modules/configuration/domain/usecases/update-configuration.usecase.ts @@ -11,40 +11,42 @@ import { Configuration } from '../entities/configuration'; @CommandHandler(UpdateConfigurationCommand) export class UpdateConfigurationUseCase { constructor( - private readonly _repository: ConfigurationRepository, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly repository: ConfigurationRepository, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} - async execute(command: UpdateConfigurationCommand): Promise { - const entity = this._mapper.map( + execute = async ( + command: UpdateConfigurationCommand, + ): Promise => { + const entity = this.mapper.map( command.updateConfigurationRequest, UpdateConfigurationRequest, Configuration, ); try { - const configuration = await this._repository.update( + const configuration = await this.repository.update( command.updateConfigurationRequest.uuid, entity, ); - this._messager.publish( + this.messager.publish( 'configuration.update', JSON.stringify( - this._mapper.map( + this.mapper.map( configuration, Configuration, ConfigurationMessagerPresenter, ), ), ); - this._messager.publish( + this.messager.publish( 'logging.configuration.update.info', JSON.stringify(command.updateConfigurationRequest), ); return configuration; } catch (error) { - this._messager.publish( + this.messager.publish( 'logging.configuration.update.crit', JSON.stringify({ command, @@ -53,5 +55,5 @@ export class UpdateConfigurationUseCase { ); throw error; } - } + }; } diff --git a/src/modules/configuration/tests/integration/configuration.repository.spec.ts b/src/modules/configuration/tests/integration/configuration.repository.spec.ts index 7e5afb0..13d9d55 100644 --- a/src/modules/configuration/tests/integration/configuration.repository.spec.ts +++ b/src/modules/configuration/tests/integration/configuration.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 { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { Domain } from '../../domain/dtos/domain.enum'; import { Configuration } from '../../domain/entities/configuration'; diff --git a/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts b/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts index bd5f2a3..e07cd27 100644 --- a/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts +++ b/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts @@ -1,7 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { ICollection } from 'src/modules/database/interfaces/collection.interface'; import { Messager } from '../../adapters/secondaries/messager'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; diff --git a/src/modules/configuration/tests/unit/find-all-configurations.usecase.spec.ts b/src/modules/configuration/tests/unit/find-all-configurations.usecase.spec.ts index 4a6e0f8..a615d5c 100644 --- a/src/modules/configuration/tests/unit/find-all-configurations.usecase.spec.ts +++ b/src/modules/configuration/tests/unit/find-all-configurations.usecase.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { ICollection } from 'src/modules/database/interfaces/collection.interface'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { Domain } from '../../domain/dtos/domain.enum'; import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request'; diff --git a/src/modules/configuration/tests/unit/propagate-configurations.usecase.spec.ts b/src/modules/configuration/tests/unit/propagate-configurations.usecase.spec.ts index 91fc653..4d9e1ca 100644 --- a/src/modules/configuration/tests/unit/propagate-configurations.usecase.spec.ts +++ b/src/modules/configuration/tests/unit/propagate-configurations.usecase.spec.ts @@ -1,7 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { ICollection } from 'src/modules/database/interfaces/collection.interface'; import { Messager } from '../../adapters/secondaries/messager'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { Domain } from '../../domain/dtos/domain.enum'; 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 6cf2419..6a858ea 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 { ConfigRepository } from './src/domain/configuration.repository'; +import { PrismaService } from './adapters/secondaries/prisma-service'; +import { ConfigRepository } from './domain/configuration.repository'; @Module({ providers: [PrismaService, ConfigRepository], diff --git a/src/modules/database/src/domain/configuration.repository.ts b/src/modules/database/domain/configuration.repository.ts similarity index 100% rename from src/modules/database/src/domain/configuration.repository.ts rename to src/modules/database/domain/configuration.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 7e00141..0000000 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ /dev/null @@ -1,200 +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(); - } - } - } - - 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 f155339..b58c48c 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.configuration.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..cd7e7ef 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, + private readonly amqpConnection: AmqpConnection, 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 ec6cd78..5d325ff 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 { ConfigurationRepository } from '../../../configuration/adapters/seconda @Injectable() export class PrismaHealthIndicatorUseCase extends HealthIndicator { - constructor(private readonly _repository: ConfigurationRepository) { + constructor(private readonly repository: ConfigurationRepository) { 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, }); } - } + }; }