Merge branch 'clean' into 'main'

clean using es6

See merge request v3/service/configuration!22
This commit is contained in:
Sylvain Briat 2023-05-05 14:09:32 +00:00
commit 169941348c
27 changed files with 495 additions and 321 deletions

View File

@ -12,8 +12,12 @@ WORKDIR /usr/src/app
# Copying this first prevents re-running npm install on every code change. # Copying this first prevents re-running npm install on every code change.
COPY --chown=node:node package*.json ./ 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` # Install app dependencies using the `npm ci` command instead of `npm install`
RUN npm ci RUN npm ci
RUN npx prisma generate
# Bundle app source # Bundle app source
COPY --chown=node:node . . COPY --chown=node:node . .

View File

@ -3,8 +3,8 @@ import { InjectMapper } from '@automapper/nestjs';
import { Controller, UsePipes } from '@nestjs/common'; import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { DatabaseException } from 'src/modules/database/src/exceptions/database.exception'; import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; import { ICollection } from 'src/modules/database/interfaces/collection.interface';
import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe'; import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe';
import { CreateConfigurationCommand } from '../../commands/create-configuration.command'; import { CreateConfigurationCommand } from '../../commands/create-configuration.command';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
@ -28,21 +28,21 @@ import { ConfigurationPresenter } from './configuration.presenter';
@Controller() @Controller()
export class ConfigurationController { export class ConfigurationController {
constructor( constructor(
private readonly _commandBus: CommandBus, private readonly commandBus: CommandBus,
private readonly _queryBus: QueryBus, private readonly queryBus: QueryBus,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
@GrpcMethod('ConfigurationService', 'FindAll') @GrpcMethod('ConfigurationService', 'FindAll')
async findAll( async findAll(
data: FindAllConfigurationsRequest, data: FindAllConfigurationsRequest,
): Promise<ICollection<Configuration>> { ): Promise<ICollection<Configuration>> {
const configurationCollection = await this._queryBus.execute( const configurationCollection = await this.queryBus.execute(
new FindAllConfigurationsQuery(data), new FindAllConfigurationsQuery(data),
); );
return Promise.resolve({ return Promise.resolve({
data: configurationCollection.data.map((configuration: Configuration) => data: configurationCollection.data.map((configuration: Configuration) =>
this._mapper.map(configuration, Configuration, ConfigurationPresenter), this.mapper.map(configuration, Configuration, ConfigurationPresenter),
), ),
total: configurationCollection.total, total: configurationCollection.total,
}); });
@ -53,10 +53,10 @@ export class ConfigurationController {
data: FindConfigurationByUuidRequest, data: FindConfigurationByUuidRequest,
): Promise<ConfigurationPresenter> { ): Promise<ConfigurationPresenter> {
try { try {
const configuration = await this._queryBus.execute( const configuration = await this.queryBus.execute(
new FindConfigurationByUuidQuery(data), new FindConfigurationByUuidQuery(data),
); );
return this._mapper.map( return this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationPresenter, ConfigurationPresenter,
@ -74,10 +74,10 @@ export class ConfigurationController {
data: CreateConfigurationRequest, data: CreateConfigurationRequest,
): Promise<ConfigurationPresenter> { ): Promise<ConfigurationPresenter> {
try { try {
const configuration = await this._commandBus.execute( const configuration = await this.commandBus.execute(
new CreateConfigurationCommand(data), new CreateConfigurationCommand(data),
); );
return this._mapper.map( return this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationPresenter, ConfigurationPresenter,
@ -100,11 +100,11 @@ export class ConfigurationController {
data: UpdateConfigurationRequest, data: UpdateConfigurationRequest,
): Promise<ConfigurationPresenter> { ): Promise<ConfigurationPresenter> {
try { try {
const configuration = await this._commandBus.execute( const configuration = await this.commandBus.execute(
new UpdateConfigurationCommand(data), new UpdateConfigurationCommand(data),
); );
return this._mapper.map( return this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationPresenter, ConfigurationPresenter,
@ -127,7 +127,7 @@ export class ConfigurationController {
data: FindConfigurationByUuidRequest, data: FindConfigurationByUuidRequest,
): Promise<void> { ): Promise<void> {
try { try {
await this._commandBus.execute(new DeleteConfigurationCommand(data.uuid)); await this.commandBus.execute(new DeleteConfigurationCommand(data.uuid));
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
@ -146,7 +146,7 @@ export class ConfigurationController {
@GrpcMethod('ConfigurationService', 'Propagate') @GrpcMethod('ConfigurationService', 'Propagate')
async propagate(): Promise<void> { async propagate(): Promise<void> {
try { try {
await this._queryBus.execute(new PropagateConfigurationsQuery()); await this.queryBus.execute(new PropagateConfigurationsQuery());
} catch (e) { } catch (e) {
throw new RpcException({}); throw new RpcException({});
} }

View File

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; 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'; import { Configuration } from '../../domain/entities/configuration';
@Injectable() @Injectable()
export class ConfigurationRepository extends ConfigRepository<Configuration> { export class ConfigurationRepository extends ConfigRepository<Configuration> {
protected _model = 'configuration'; protected model = 'configuration';
} }

View File

@ -6,13 +6,13 @@ import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable() @Injectable()
export class Messager extends IMessageBroker { export class Messager extends IMessageBroker {
constructor( constructor(
private readonly _amqpConnection: AmqpConnection, private readonly amqpConnection: AmqpConnection,
configService: ConfigService, private readonly configService: ConfigService,
) { ) {
super(configService.get<string>('RMQ_EXCHANGE')); super(configService.get<string>('RMQ_EXCHANGE'));
} }
publish(routingKey: string, message: string): void { publish = (routingKey: string, message: string): void => {
this._amqpConnection.publish(this.exchange, routingKey, message); this.amqpConnection.publish(this.exchange, routingKey, message);
} };
} }

View File

@ -11,31 +11,33 @@ import { Configuration } from '../entities/configuration';
@CommandHandler(CreateConfigurationCommand) @CommandHandler(CreateConfigurationCommand)
export class CreateConfigurationUseCase { export class CreateConfigurationUseCase {
constructor( constructor(
private readonly _repository: ConfigurationRepository, private readonly repository: ConfigurationRepository,
private readonly _messager: Messager, private readonly messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
async execute(command: CreateConfigurationCommand): Promise<Configuration> { execute = async (
const entity = this._mapper.map( command: CreateConfigurationCommand,
): Promise<Configuration> => {
const entity = this.mapper.map(
command.createConfigurationRequest, command.createConfigurationRequest,
CreateConfigurationRequest, CreateConfigurationRequest,
Configuration, Configuration,
); );
try { try {
const configuration = await this._repository.create(entity); const configuration = await this.repository.create(entity);
this._messager.publish( this.messager.publish(
'configuration.create', 'configuration.create',
JSON.stringify( JSON.stringify(
this._mapper.map( this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationMessagerPresenter, ConfigurationMessagerPresenter,
), ),
), ),
); );
this._messager.publish( this.messager.publish(
'logging.configuration.create.info', 'logging.configuration.create.info',
JSON.stringify(configuration), JSON.stringify(configuration),
); );
@ -45,7 +47,7 @@ export class CreateConfigurationUseCase {
if (error.message.includes('Already exists')) { if (error.message.includes('Already exists')) {
key = 'logging.configuration.create.warning'; key = 'logging.configuration.create.warning';
} }
this._messager.publish( this.messager.publish(
key, key,
JSON.stringify({ JSON.stringify({
command, command,
@ -54,5 +56,5 @@ export class CreateConfigurationUseCase {
); );
throw error; throw error;
} }
} };
} }

View File

@ -10,31 +10,33 @@ import { Configuration } from '../entities/configuration';
@CommandHandler(DeleteConfigurationCommand) @CommandHandler(DeleteConfigurationCommand)
export class DeleteConfigurationUseCase { export class DeleteConfigurationUseCase {
constructor( constructor(
private readonly _repository: ConfigurationRepository, private readonly repository: ConfigurationRepository,
private readonly _messager: Messager, private readonly messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
async execute(command: DeleteConfigurationCommand): Promise<Configuration> { execute = async (
command: DeleteConfigurationCommand,
): Promise<Configuration> => {
try { try {
const configuration = await this._repository.delete(command.uuid); const configuration = await this.repository.delete(command.uuid);
this._messager.publish( this.messager.publish(
'configuration.delete', 'configuration.delete',
JSON.stringify( JSON.stringify(
this._mapper.map( this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationMessagerPresenter, ConfigurationMessagerPresenter,
), ),
), ),
); );
this._messager.publish( this.messager.publish(
'logging.configuration.delete.info', 'logging.configuration.delete.info',
JSON.stringify({ uuid: configuration.uuid }), JSON.stringify({ uuid: configuration.uuid }),
); );
return configuration; return configuration;
} catch (error) { } catch (error) {
this._messager.publish( this.messager.publish(
'logging.configuration.delete.crit', 'logging.configuration.delete.crit',
JSON.stringify({ JSON.stringify({
command, command,
@ -43,5 +45,5 @@ export class DeleteConfigurationUseCase {
); );
throw error; throw error;
} }
} };
} }

View File

@ -1,19 +1,18 @@
import { QueryHandler } from '@nestjs/cqrs'; 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 { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query'; import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
import { Configuration } from '../entities/configuration'; import { Configuration } from '../entities/configuration';
@QueryHandler(FindAllConfigurationsQuery) @QueryHandler(FindAllConfigurationsQuery)
export class FindAllConfigurationsUseCase { export class FindAllConfigurationsUseCase {
constructor(private readonly _repository: ConfigurationRepository) {} constructor(private readonly repository: ConfigurationRepository) {}
async execute( execute = async (
findAllConfigurationsQuery: FindAllConfigurationsQuery, findAllConfigurationsQuery: FindAllConfigurationsQuery,
): Promise<ICollection<Configuration>> { ): Promise<ICollection<Configuration>> =>
return this._repository.findAll( this.repository.findAll(
findAllConfigurationsQuery.page, findAllConfigurationsQuery.page,
findAllConfigurationsQuery.perPage, findAllConfigurationsQuery.perPage,
); );
}
} }

View File

@ -8,21 +8,21 @@ import { Configuration } from '../entities/configuration';
@QueryHandler(FindConfigurationByUuidQuery) @QueryHandler(FindConfigurationByUuidQuery)
export class FindConfigurationByUuidUseCase { export class FindConfigurationByUuidUseCase {
constructor( constructor(
private readonly _repository: ConfigurationRepository, private readonly repository: ConfigurationRepository,
private readonly _messager: Messager, private readonly messager: Messager,
) {} ) {}
async execute( execute = async (
findConfigurationByUuid: FindConfigurationByUuidQuery, findConfigurationByUuid: FindConfigurationByUuidQuery,
): Promise<Configuration> { ): Promise<Configuration> => {
try { try {
const configuration = await this._repository.findOneByUuid( const configuration = await this.repository.findOneByUuid(
findConfigurationByUuid.uuid, findConfigurationByUuid.uuid,
); );
if (!configuration) throw new NotFoundException(); if (!configuration) throw new NotFoundException();
return configuration; return configuration;
} catch (error) { } catch (error) {
this._messager.publish( this.messager.publish(
'logging.configuration.read.warning', 'logging.configuration.read.warning',
JSON.stringify({ JSON.stringify({
query: findConfigurationByUuid, query: findConfigurationByUuid,
@ -31,5 +31,5 @@ export class FindConfigurationByUuidUseCase {
); );
throw error; throw error;
} }
} };
} }

View File

@ -10,20 +10,22 @@ import { Configuration } from '../entities/configuration';
@QueryHandler(PropagateConfigurationsQuery) @QueryHandler(PropagateConfigurationsQuery)
export class PropagateConfigurationsUseCase { export class PropagateConfigurationsUseCase {
constructor( constructor(
private readonly _repository: ConfigurationRepository, private readonly repository: ConfigurationRepository,
private readonly _messager: Messager, private readonly messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars execute = async (
async execute(propagateConfigurationsQuery: PropagateConfigurationsQuery) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
propagateConfigurationsQuery: PropagateConfigurationsQuery,
) => {
try { try {
const configurations = await this._repository.findAll(1, 999999); const configurations = await this.repository.findAll(1, 999999);
this._messager.publish( this.messager.publish(
'configuration.propagate', 'configuration.propagate',
JSON.stringify( JSON.stringify(
configurations.data.map((configuration) => configurations.data.map((configuration) =>
this._mapper.map( this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationMessagerPresenter, ConfigurationMessagerPresenter,
@ -31,16 +33,10 @@ export class PropagateConfigurationsUseCase {
), ),
), ),
); );
this._messager.publish( this.messager.publish('logging.configuration.update.info', 'propagation');
'logging.configuration.update.info',
'propagation',
);
} catch (error) { } catch (error) {
this._messager.publish( this.messager.publish('logging.configuration.update.crit', 'propagation');
'logging.configuration.update.crit',
'propagation',
);
throw error; throw error;
} }
} };
} }

View File

@ -11,40 +11,42 @@ import { Configuration } from '../entities/configuration';
@CommandHandler(UpdateConfigurationCommand) @CommandHandler(UpdateConfigurationCommand)
export class UpdateConfigurationUseCase { export class UpdateConfigurationUseCase {
constructor( constructor(
private readonly _repository: ConfigurationRepository, private readonly repository: ConfigurationRepository,
private readonly _messager: Messager, private readonly messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
async execute(command: UpdateConfigurationCommand): Promise<Configuration> { execute = async (
const entity = this._mapper.map( command: UpdateConfigurationCommand,
): Promise<Configuration> => {
const entity = this.mapper.map(
command.updateConfigurationRequest, command.updateConfigurationRequest,
UpdateConfigurationRequest, UpdateConfigurationRequest,
Configuration, Configuration,
); );
try { try {
const configuration = await this._repository.update( const configuration = await this.repository.update(
command.updateConfigurationRequest.uuid, command.updateConfigurationRequest.uuid,
entity, entity,
); );
this._messager.publish( this.messager.publish(
'configuration.update', 'configuration.update',
JSON.stringify( JSON.stringify(
this._mapper.map( this.mapper.map(
configuration, configuration,
Configuration, Configuration,
ConfigurationMessagerPresenter, ConfigurationMessagerPresenter,
), ),
), ),
); );
this._messager.publish( this.messager.publish(
'logging.configuration.update.info', 'logging.configuration.update.info',
JSON.stringify(command.updateConfigurationRequest), JSON.stringify(command.updateConfigurationRequest),
); );
return configuration; return configuration;
} catch (error) { } catch (error) {
this._messager.publish( this.messager.publish(
'logging.configuration.update.crit', 'logging.configuration.update.crit',
JSON.stringify({ JSON.stringify({
command, command,
@ -53,5 +55,5 @@ export class UpdateConfigurationUseCase {
); );
throw error; throw error;
} }
} };
} }

View File

@ -1,7 +1,7 @@
import { TestingModule, Test } from '@nestjs/testing'; import { TestingModule, Test } from '@nestjs/testing';
import { DatabaseModule } from '../../../database/database.module'; import { DatabaseModule } from '../../../database/database.module';
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
import { DatabaseException } from '../../../database/src/exceptions/database.exception'; import { DatabaseException } from '../../../database/exceptions/database.exception';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';
import { Configuration } from '../../domain/entities/configuration'; import { Configuration } from '../../domain/entities/configuration';

View File

@ -1,7 +1,7 @@
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing'; 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 { Messager } from '../../adapters/secondaries/messager';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';

View File

@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; 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 { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';
import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request'; import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request';

View File

@ -1,7 +1,7 @@
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing'; 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 { Messager } from '../../adapters/secondaries/messager';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';

View File

@ -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<T> implements IRepository<T> {
protected model: string;
constructor(protected readonly prisma: PrismaService) {}
findAll = async (
page = 1,
perPage = 10,
where?: any,
include?: any,
): Promise<ICollection<T>> => {
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<T> => {
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<T> => {
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<T> | any, include?: any): Promise<T> => {
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<T>): Promise<T> => {
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<T> | any,
include?: any,
): Promise<T> => {
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<T> => {
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<void> => {
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<ICollection<T>> => {
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<number> => {
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<number> => {
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<boolean> => {
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();
}
}
};
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaService } from './src/adapters/secondaries/prisma-service'; import { PrismaService } from './adapters/secondaries/prisma-service';
import { ConfigRepository } from './src/domain/configuration.repository'; import { ConfigRepository } from './domain/configuration.repository';
@Module({ @Module({
providers: [PrismaService, ConfigRepository], providers: [PrismaService, ConfigRepository],

View File

@ -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<T> implements IRepository<T> {
protected _model: string;
constructor(protected readonly _prisma: PrismaService) {}
async findAll(
page = 1,
perPage = 10,
where?: any,
include?: any,
): Promise<ICollection<T>> {
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<T> {
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<T> {
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<T> | any, include?: any): Promise<T> {
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<T>): Promise<T> {
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<T> | any,
include?: any,
): Promise<T> {
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<T> {
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<void> {
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<boolean> {
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();
}
}
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; import { PrismaService } from '../../adapters/secondaries/prisma-service';
import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
import { DatabaseException } from '../../src/exceptions/database.exception'; import { DatabaseException } from '../../exceptions/database.exception';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; import { Prisma } from '@prisma/client';
class FakeEntity { class FakeEntity {
uuid?: string; uuid?: string;
@ -41,7 +41,7 @@ Array.from({ length: 10 }).forEach(() => {
@Injectable() @Injectable()
class FakePrismaRepository extends PrismaRepository<FakeEntity> { class FakePrismaRepository extends PrismaRepository<FakeEntity> {
protected _model = 'fake'; protected model = 'fake';
} }
class FakePrismaService extends PrismaService { class FakePrismaService extends PrismaService {
@ -57,10 +57,40 @@ const mockPrismaService = {
return Promise.resolve([fakeEntities, fakeEntities.length]); 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 $queryRaw: jest
.fn() .fn()
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -69,7 +99,7 @@ const mockPrismaService = {
return true; return true;
}) })
.mockImplementation(() => { .mockImplementation(() => {
throw new PrismaClientKnownRequestError('Database unavailable', { throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -80,7 +110,7 @@ const mockPrismaService = {
.mockResolvedValueOnce(fakeEntityCreated) .mockResolvedValueOnce(fakeEntityCreated)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -109,7 +139,7 @@ const mockPrismaService = {
} }
if (!entity && params?.where?.uuid == 'unknown') { if (!entity && params?.where?.uuid == 'unknown') {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -131,7 +161,7 @@ const mockPrismaService = {
}) })
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -145,14 +175,14 @@ const mockPrismaService = {
.fn() .fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
}) })
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -182,7 +212,7 @@ const mockPrismaService = {
.fn() .fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', clientVersion: 'version',
}); });
@ -206,7 +236,7 @@ const mockPrismaService = {
.fn() .fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => { .mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', { throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code', code: 'code',
clientVersion: 'version', 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', () => { describe('healthCheck', () => {
it('should throw a DatabaseException for client error', async () => { it('should throw a DatabaseException for client error', async () => {
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(

View File

@ -19,7 +19,7 @@ interface HealthCheckResponse {
@Controller() @Controller()
export class HealthServerController { export class HealthServerController {
constructor( constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
) {} ) {}
@GrpcMethod('Health', 'Check') @GrpcMethod('Health', 'Check')
@ -29,7 +29,7 @@ export class HealthServerController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
metadata: any, metadata: any,
): Promise<HealthCheckResponse> { ): Promise<HealthCheckResponse> {
const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy( const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy(
'prisma', 'prisma',
); );
return { return {

View File

@ -10,21 +10,21 @@ import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.healt
@Controller('health') @Controller('health')
export class HealthController { export class HealthController {
constructor( constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
private _healthCheckService: HealthCheckService, private readonly healthCheckService: HealthCheckService,
private _messager: Messager, private readonly messager: Messager,
) {} ) {}
@Get() @Get()
@HealthCheck() @HealthCheck()
async check() { async check() {
try { try {
return await this._healthCheckService.check([ return await this.healthCheckService.check([
async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'), async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'),
]); ]);
} catch (error) { } catch (error) {
const healthCheckResult: HealthCheckResult = error.response; const healthCheckResult: HealthCheckResult = error.response;
this._messager.publish( this.messager.publish(
'logging.configuration.health.crit', 'logging.configuration.health.crit',
JSON.stringify(healthCheckResult.error), JSON.stringify(healthCheckResult.error),
); );

View File

@ -6,13 +6,13 @@ import { IMessageBroker } from './message-broker';
@Injectable() @Injectable()
export class Messager extends IMessageBroker { export class Messager extends IMessageBroker {
constructor( constructor(
private readonly _amqpConnection: AmqpConnection, private readonly amqpConnection: AmqpConnection,
configService: ConfigService, configService: ConfigService,
) { ) {
super(configService.get<string>('RMQ_EXCHANGE')); super(configService.get<string>('RMQ_EXCHANGE'));
} }
publish(routingKey: string, message: string): void { publish = (routingKey: string, message: string): void => {
this._amqpConnection.publish(this.exchange, routingKey, message); this.amqpConnection.publish(this.exchange, routingKey, message);
} };
} }

View File

@ -8,18 +8,18 @@ import { ConfigurationRepository } from '../../../configuration/adapters/seconda
@Injectable() @Injectable()
export class PrismaHealthIndicatorUseCase extends HealthIndicator { export class PrismaHealthIndicatorUseCase extends HealthIndicator {
constructor(private readonly _repository: ConfigurationRepository) { constructor(private readonly repository: ConfigurationRepository) {
super(); super();
} }
async isHealthy(key: string): Promise<HealthIndicatorResult> { isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
try { try {
await this._repository.healthCheck(); await this.repository.healthCheck();
return this.getStatus(key, true); return this.getStatus(key, true);
} catch (e) { } catch (e) {
throw new HealthCheckError('Prisma', { throw new HealthCheckError('Prisma', {
prisma: e.message, prisma: e.message,
}); });
} }
} };
} }