From b232247c93bb189e1a031c2be0aa62c816813fff Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 21 Jun 2023 11:50:36 +0200 Subject: [PATCH] working version, with basic tests --- package.json | 21 +- src/libs/db/prisma-repository.base.ts | 35 +- .../{prisma-service.ts => prisma.service.ts} | 0 src/libs/exceptions/exception.codes.ts | 1 + src/libs/exceptions/exceptions.ts | 17 + src/main.ts | 7 +- src/modules/ad/ad.module.ts | 10 +- .../ad/infrastructure/ad.repository.ts | 6 +- .../ad/infrastructure/prisma-service.ts | 15 - .../interface/{ => grpc-controllers}/ad.proto | 0 .../domain/create-ad.usecase.spec.ts | 0 .../domain/find-ad-by-uuid.usecase.spec.ts | 0 .../domain/frequency-normalizer.spec.ts | 0 .../domain/frequency.mapping.spec.ts | 0 .../domain/is-punctual-or-recurrent.spec.ts | 0 .../domain/valid-position-indexes.spec.ts | 0 .../tests/unit/core/create-ad.service.spec.ts | 10 +- .../default-param.provider.spec.ts | 32 +- .../message-publisher.spec.ts | 4 +- .../infrastructure/timezone-finder.spec.ts | 14 + .../secondaries/prisma-repository.abstract.ts | 258 -------- .../adapters/secondaries/prisma-service.ts | 15 - src/modules/database/database.module.ts | 9 - src/modules/database/domain/ad-repository.ts | 3 - .../database/exceptions/database.exception.ts | 24 - .../interfaces/collection.interface.ts | 4 - .../interfaces/repository.interface.ts | 18 - .../tests/unit/prisma-repository.spec.ts | 571 ------------------ .../adapters/primaries/health.controller.ts | 37 -- .../core/ports/check-repository.port.ts | 3 + .../repositories.health-indicator.usecase.ts | 48 ++ .../interfaces/check-repository.interface.ts | 3 - .../repositories.health-indicator.usecase.ts | 33 - src/modules/health/health.constants.ts | 1 + src/modules/health/health.di-tokens.ts | 1 + src/modules/health/health.module.ts | 19 +- .../message-publisher.ts | 2 +- .../health.grpc.controller.ts} | 4 +- .../grpc-controllers}/health.proto | 0 .../health.http.controller.ts | 24 + .../tests/unit/message-publisher.spec.ts | 4 +- ...ositories.health-indicator.usecase.spec.ts | 23 +- tsconfig.json | 1 + 43 files changed, 217 insertions(+), 1060 deletions(-) rename src/libs/db/{prisma-service.ts => prisma.service.ts} (100%) delete mode 100644 src/modules/ad/infrastructure/prisma-service.ts rename src/modules/ad/interface/{ => grpc-controllers}/ad.proto (100%) rename src/modules/ad/tests/{unit => }/domain/create-ad.usecase.spec.ts (100%) rename src/modules/ad/tests/{unit => }/domain/find-ad-by-uuid.usecase.spec.ts (100%) rename src/modules/ad/tests/{unit => }/domain/frequency-normalizer.spec.ts (100%) rename src/modules/ad/tests/{unit => }/domain/frequency.mapping.spec.ts (100%) rename src/modules/ad/tests/{unit => }/domain/is-punctual-or-recurrent.spec.ts (100%) rename src/modules/ad/tests/{unit => }/domain/valid-position-indexes.spec.ts (100%) rename src/modules/ad/tests/unit/{adapters/secondaries => infrastructure}/default-param.provider.spec.ts (51%) rename src/modules/ad/tests/unit/{adapters/secondaries => infrastructure}/message-publisher.spec.ts (84%) create mode 100644 src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts delete mode 100644 src/modules/database/adapters/secondaries/prisma-repository.abstract.ts delete mode 100644 src/modules/database/adapters/secondaries/prisma-service.ts delete mode 100644 src/modules/database/database.module.ts delete mode 100644 src/modules/database/domain/ad-repository.ts delete mode 100644 src/modules/database/exceptions/database.exception.ts delete mode 100644 src/modules/database/interfaces/collection.interface.ts delete mode 100644 src/modules/database/interfaces/repository.interface.ts delete mode 100644 src/modules/database/tests/unit/prisma-repository.spec.ts delete mode 100644 src/modules/health/adapters/primaries/health.controller.ts create mode 100644 src/modules/health/core/ports/check-repository.port.ts create mode 100644 src/modules/health/core/usecases/repositories.health-indicator.usecase.ts delete mode 100644 src/modules/health/domain/interfaces/check-repository.interface.ts delete mode 100644 src/modules/health/domain/usecases/repositories.health-indicator.usecase.ts create mode 100644 src/modules/health/health.constants.ts create mode 100644 src/modules/health/health.di-tokens.ts rename src/modules/health/{adapters/secondaries => infrastructure}/message-publisher.ts (88%) rename src/modules/health/{adapters/primaries/health-server.controller.ts => interface/grpc-controllers/health.grpc.controller.ts} (86%) rename src/modules/health/{adapters/primaries => interface/grpc-controllers}/health.proto (100%) create mode 100644 src/modules/health/interface/http-controllers/health.http.controller.ts diff --git a/package.json b/package.json index 310dcf9..dc70fd3 100644 --- a/package.json +++ b/package.json @@ -94,17 +94,14 @@ "ts" ], "modulePathIgnorePatterns": [ - ".controller.ts", + "libs/", ".module.ts", - ".request.ts", - ".presenter.ts", - ".profile.ts", - ".exception.ts", + ".dto.ts", ".constants.ts", "main.ts" ], "rootDir": "src", - "testRegex": ".*\\.service.spec\\.ts$", + "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, @@ -112,21 +109,17 @@ "**/*.(t|j)s" ], "coveragePathIgnorePatterns": [ - ".validator.ts", - ".controller.ts", + "libs/", ".module.ts", - ".request.ts", - ".presenter.ts", - ".profile.ts", - ".exception.ts", + ".dto.ts", ".constants.ts", - ".interfaces.ts", "main.ts" ], "coverageDirectory": "../coverage", "moduleNameMapper": { "^@libs(.*)": "/libs/$1", - "^@modules(.*)": "/modules/$1" + "^@modules(.*)": "/modules/$1", + "^@src(.*)": "$1" }, "testEnvironment": "node" } diff --git a/src/libs/db/prisma-repository.base.ts b/src/libs/db/prisma-repository.base.ts index 4a30d09..a6ba546 100644 --- a/src/libs/db/prisma-repository.base.ts +++ b/src/libs/db/prisma-repository.base.ts @@ -4,6 +4,8 @@ import { ObjectLiteral } from '../types'; import { LoggerPort } from '../ports/logger.port'; import { None, Option, Some } from 'oxide.ts'; import { PrismaRepositoryPort } from '../ports/prisma-repository.port'; +import { Prisma } from '@prisma/client'; +import { ConflictException, DatabaseErrorException } from '@libs/exceptions'; export abstract class PrismaRepositoryBase< Aggregate extends AggregateRoot, @@ -17,16 +19,11 @@ export abstract class PrismaRepositoryBase< protected readonly logger: LoggerPort, ) {} - async findOneById(uuid: string): Promise> { - try { - const entity = await this.prisma.findUnique({ - where: { uuid }, - }); - - return entity ? Some(this.mapper.toDomain(entity)) : None; - } catch (e) { - console.log('ouch on findOneById'); - } + async findOneById(id: string): Promise> { + const entity = await this.prisma.findUnique({ + where: { uuid: id }, + }); + return entity ? Some(this.mapper.toDomain(entity)) : None; } async insert(entity: Aggregate): Promise { @@ -35,12 +32,24 @@ export abstract class PrismaRepositoryBase< data: this.mapper.toPersistence(entity), }); } catch (e) { - console.log(e); - console.log('ouch on insert'); + if (e instanceof Prisma.PrismaClientKnownRequestError) { + if (e.message.includes('Already exists')) { + throw new ConflictException('Record already exists', e); + } + } + throw e; } } async healthCheck(): Promise { - return true; + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseErrorException(e.message); + } + throw new DatabaseErrorException(); + } } } diff --git a/src/libs/db/prisma-service.ts b/src/libs/db/prisma.service.ts similarity index 100% rename from src/libs/db/prisma-service.ts rename to src/libs/db/prisma.service.ts diff --git a/src/libs/exceptions/exception.codes.ts b/src/libs/exceptions/exception.codes.ts index 3291bfb..5ca541b 100644 --- a/src/libs/exceptions/exception.codes.ts +++ b/src/libs/exceptions/exception.codes.ts @@ -13,3 +13,4 @@ export const ARGUMENT_NOT_PROVIDED = 'GENERIC.ARGUMENT_NOT_PROVIDED'; export const NOT_FOUND = 'GENERIC.NOT_FOUND'; export const CONFLICT = 'GENERIC.CONFLICT'; export const INTERNAL_SERVER_ERROR = 'GENERIC.INTERNAL_SERVER_ERROR'; +export const DATABASE_ERROR = 'GENERIC.DATABASE_ERROR'; diff --git a/src/libs/exceptions/exceptions.ts b/src/libs/exceptions/exceptions.ts index 7044dbd..ceda019 100644 --- a/src/libs/exceptions/exceptions.ts +++ b/src/libs/exceptions/exceptions.ts @@ -3,6 +3,7 @@ import { ARGUMENT_NOT_PROVIDED, ARGUMENT_OUT_OF_RANGE, CONFLICT, + DATABASE_ERROR, INTERNAL_SERVER_ERROR, NOT_FOUND, } from '.'; @@ -80,3 +81,19 @@ export class InternalServerErrorException extends ExceptionBase { readonly code = INTERNAL_SERVER_ERROR; } + +/** + * Used to indicate a database error + * + * @class DatabaseErrorException + * @extends {ExceptionBase} + */ +export class DatabaseErrorException extends ExceptionBase { + static readonly message = 'Database error'; + + constructor(message = DatabaseErrorException.message) { + super(message); + } + + readonly code = DATABASE_ERROR; +} diff --git a/src/main.ts b/src/main.ts index 1c06331..505ada1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,8 +13,11 @@ async function bootstrap() { options: { package: ['ad', 'health'], protoPath: [ - join(__dirname, 'modules/ad/interface/ad.proto'), - join(__dirname, 'modules/health/adapters/primaries/health.proto'), + join(__dirname, 'modules/ad/interface/grpc-controllers/ad.proto'), + join( + __dirname, + 'modules/health/interface/grpc-controllers/health.proto', + ), ], url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, loader: { keepCase: true }, diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 505e5df..e81865a 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -1,24 +1,26 @@ import { Module } from '@nestjs/common'; import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller'; -import { DatabaseModule } from '../database/database.module'; import { CqrsModule } from '@nestjs/cqrs'; import { AD_REPOSITORY, PARAMS_PROVIDER, TIMEZONE_FINDER, } from './ad.di-tokens'; -import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants'; +import { + MESSAGE_BROKER_PUBLISHER, + MESSAGE_PUBLISHER, +} from '@src/app.constants'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; import { DefaultParamsProvider } from './infrastructure/default-params-provider'; import { MessagePublisher } from './infrastructure/message-publisher'; -import { PrismaService } from './infrastructure/prisma-service'; import { AdMapper } from './ad.mapper'; import { CreateAdService } from './core/commands/create-ad/create-ad.service'; import { TimezoneFinder } from './infrastructure/timezone-finder'; +import { PrismaService } from '@libs/db/prisma.service'; @Module({ - imports: [DatabaseModule, CqrsModule], + imports: [CqrsModule], controllers: [CreateAdGrpcController], providers: [ CreateAdService, diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 2b80a1e..1a187ef 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,10 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { AdRepositoryPort } from '../core/ports/ad.repository.port'; import { AdEntity } from '../core/ad.entity'; -import { PrismaRepositoryBase } from '@libs/db/prisma-repository.base'; +import { AdRepositoryPort } from '../core/ports/ad.repository.port'; +import { PrismaService } from '@libs/db/prisma.service'; import { AdMapper } from '../ad.mapper'; -import { PrismaService } from './prisma-service'; +import { PrismaRepositoryBase } from '@libs/db/prisma-repository.base'; export type AdModel = { uuid: string; diff --git a/src/modules/ad/infrastructure/prisma-service.ts b/src/modules/ad/infrastructure/prisma-service.ts deleted file mode 100644 index edf6532..0000000 --- a/src/modules/ad/infrastructure/prisma-service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; - -@Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { - async onModuleInit() { - await this.$connect(); - } - - async enableShutdownHooks(app: INestApplication) { - this.$on('beforeExit', async () => { - await app.close(); - }); - } -} diff --git a/src/modules/ad/interface/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto similarity index 100% rename from src/modules/ad/interface/ad.proto rename to src/modules/ad/interface/grpc-controllers/ad.proto diff --git a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts b/src/modules/ad/tests/domain/create-ad.usecase.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts rename to src/modules/ad/tests/domain/create-ad.usecase.spec.ts diff --git a/src/modules/ad/tests/unit/domain/find-ad-by-uuid.usecase.spec.ts b/src/modules/ad/tests/domain/find-ad-by-uuid.usecase.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/find-ad-by-uuid.usecase.spec.ts rename to src/modules/ad/tests/domain/find-ad-by-uuid.usecase.spec.ts diff --git a/src/modules/ad/tests/unit/domain/frequency-normalizer.spec.ts b/src/modules/ad/tests/domain/frequency-normalizer.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/frequency-normalizer.spec.ts rename to src/modules/ad/tests/domain/frequency-normalizer.spec.ts diff --git a/src/modules/ad/tests/unit/domain/frequency.mapping.spec.ts b/src/modules/ad/tests/domain/frequency.mapping.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/frequency.mapping.spec.ts rename to src/modules/ad/tests/domain/frequency.mapping.spec.ts diff --git a/src/modules/ad/tests/unit/domain/is-punctual-or-recurrent.spec.ts b/src/modules/ad/tests/domain/is-punctual-or-recurrent.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/is-punctual-or-recurrent.spec.ts rename to src/modules/ad/tests/domain/is-punctual-or-recurrent.spec.ts diff --git a/src/modules/ad/tests/unit/domain/valid-position-indexes.spec.ts b/src/modules/ad/tests/domain/valid-position-indexes.spec.ts similarity index 100% rename from src/modules/ad/tests/unit/domain/valid-position-indexes.spec.ts rename to src/modules/ad/tests/domain/valid-position-indexes.spec.ts diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index f6332a8..d46d236 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -9,6 +9,7 @@ import { CreateAdCommand } from '@modules/ad/core/commands/create-ad/create-ad.c import { Result } from 'oxide.ts'; import { AggregateID } from '@libs/ddd'; import { AdAlreadyExistsError } from '@modules/ad/core/ad.errors'; +import { AdEntity } from '@modules/ad/core/ad.entity'; const originWaypoint: WaypointDTO = { position: 0, @@ -43,11 +44,7 @@ const punctualCreateAdRequest: CreateAdRequestDTO = { }; const mockAdRepository = { - insert: jest.fn().mockImplementationOnce(() => { - return Promise.resolve({ - uuid: '047a6ecf-23d4-4d3e-877c-3225d560a8da', - }); - }), + insert: jest.fn(), }; const mockDefaultParamsProvider: DefaultParamsProviderPort = { @@ -98,6 +95,9 @@ describe('create-ad.service', () => { describe('execution', () => { const createAdCommand = new CreateAdCommand(punctualCreateAdRequest); it('should create a new ad', async () => { + AdEntity.create = jest.fn().mockReturnValue({ + id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', + }); const result: Result = await createAdService.execute(createAdCommand); expect(result.unwrap()).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts similarity index 51% rename from src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts rename to src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts index 15918cd..57ad3d8 100644 --- a/src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -1,12 +1,29 @@ +import { DefaultParams } from '@modules/ad/core/ports/default-params.type'; +import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params-provider'; -import { DefaultParams } from '../../../../core/ports/default-params.type'; const mockConfigService = { - get: jest.fn().mockImplementation(() => 'some_default_value'), + get: jest.fn().mockImplementation((value: string) => { + switch (value) { + case 'DEPARTURE_MARGIN': + return 900; + case 'ROLE': + return 'passenger'; + case 'SEATS_PROPOSED': + return 3; + case 'SEATS_REQUESTED': + return 1; + case 'STRICT_FREQUENCY': + return 'false'; + case 'DEFAULT_TIMEZONE': + return 'Europe/Paris'; + default: + return 'some_default_value'; + } + }), }; -//TODO complete coverage + describe('DefaultParamsProvider', () => { let defaultParamsProvider: DefaultParamsProvider; @@ -33,8 +50,9 @@ describe('DefaultParamsProvider', () => { it('should provide default params', async () => { const params: DefaultParams = defaultParamsProvider.getParams(); - expect(params.SUN_MARGIN).toBeNaN(); - expect(params.PASSENGER).toBe(false); - expect(params.DRIVER).toBe(false); + expect(params.SUN_MARGIN).toBe(900); + expect(params.PASSENGER).toBeTruthy(); + expect(params.DRIVER).toBeFalsy(); + expect(params.DEFAULT_TIMEZONE).toBe('Europe/Paris'); }); }); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/message-publisher.spec.ts b/src/modules/ad/tests/unit/infrastructure/message-publisher.spec.ts similarity index 84% rename from src/modules/ad/tests/unit/adapters/secondaries/message-publisher.spec.ts rename to src/modules/ad/tests/unit/infrastructure/message-publisher.spec.ts index d32a536..54dab1f 100644 --- a/src/modules/ad/tests/unit/adapters/secondaries/message-publisher.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/message-publisher.spec.ts @@ -1,6 +1,6 @@ +import { MessagePublisher } from '@modules/ad/infrastructure/message-publisher'; import { Test, TestingModule } from '@nestjs/testing'; -import { MessagePublisher } from '../../../../adapters/secondaries/message-publisher'; -import { MESSAGE_BROKER_PUBLISHER } from '../../../../../../app.constants'; +import { MESSAGE_BROKER_PUBLISHER } from '@src/app.constants'; const mockMessageBrokerPublisher = { publish: jest.fn().mockImplementation(), diff --git a/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts new file mode 100644 index 0000000..46e3ab8 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts @@ -0,0 +1,14 @@ +import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder'; + +describe('Timezone Finder', () => { + it('should be defined', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + expect(timezoneFinder).toBeDefined(); + }); + it('should get timezone for Nancy(France) as Europe/Paris', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + const timezones = timezoneFinder.timezones(6.179373, 48.687913); + expect(timezones.length).toBe(1); + expect(timezones[0]).toBe('Europe/Paris'); + }); +}); diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts deleted file mode 100644 index b5b9700..0000000 --- a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts +++ /dev/null @@ -1,258 +0,0 @@ -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) {} - - 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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.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 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 ? - 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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.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 Prisma.PrismaClientKnownRequestError) { - throw new DatabaseException( - Prisma.PrismaClientKnownRequestError.name, - e.code, - e.message, - ); - } else { - throw new DatabaseException(); - } - } - } - - async findAllByQuery( - 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, - }); - } - - async createWithFields(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(); - } - } - } - - async updateWithFields(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(); - } - } - } - - async healthCheck(): 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/adapters/secondaries/prisma-service.ts b/src/modules/database/adapters/secondaries/prisma-service.ts deleted file mode 100644 index edf6532..0000000 --- a/src/modules/database/adapters/secondaries/prisma-service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; - -@Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { - async onModuleInit() { - await this.$connect(); - } - - async enableShutdownHooks(app: INestApplication) { - this.$on('beforeExit', async () => { - await app.close(); - }); - } -} diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts deleted file mode 100644 index f1defa7..0000000 --- a/src/modules/database/database.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PrismaService } from './adapters/secondaries/prisma-service'; -import { AdRepository } from './domain/ad-repository'; - -@Module({ - providers: [PrismaService, AdRepository], - exports: [PrismaService, AdRepository], -}) -export class DatabaseModule {} diff --git a/src/modules/database/domain/ad-repository.ts b/src/modules/database/domain/ad-repository.ts deleted file mode 100644 index edbaf5f..0000000 --- a/src/modules/database/domain/ad-repository.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; - -export class AdRepository extends PrismaRepository {} diff --git a/src/modules/database/exceptions/database.exception.ts b/src/modules/database/exceptions/database.exception.ts deleted file mode 100644 index b0782a6..0000000 --- a/src/modules/database/exceptions/database.exception.ts +++ /dev/null @@ -1,24 +0,0 @@ -export class DatabaseException implements Error { - name: string; - message: string; - - constructor( - private _type: string = 'unknown', - private _code: string = '', - message?: string, - ) { - this.name = 'DatabaseException'; - this.message = message ?? 'An error occured with the database.'; - if (this.message.includes('Unique constraint failed')) { - this.message = 'Already exists.'; - } - } - - get type(): string { - return this._type; - } - - get code(): string { - return this._code; - } -} diff --git a/src/modules/database/interfaces/collection.interface.ts b/src/modules/database/interfaces/collection.interface.ts deleted file mode 100644 index 6e9a96d..0000000 --- a/src/modules/database/interfaces/collection.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ICollection { - data: T[]; - total: number; -} diff --git a/src/modules/database/interfaces/repository.interface.ts b/src/modules/database/interfaces/repository.interface.ts deleted file mode 100644 index 1e23984..0000000 --- a/src/modules/database/interfaces/repository.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ICollection } from './collection.interface'; - -export interface IRepository { - findAll( - page: number, - perPage: number, - params?: any, - include?: any, - ): Promise>; - findOne(where: any, include?: any): Promise; - findOneByUuid(uuid: string, include?: any): Promise; - create(entity: Partial | any, include?: any): Promise; - update(uuid: string, entity: Partial, include?: any): Promise; - updateWhere(where: any, entity: Partial | any, include?: any): Promise; - delete(uuid: string): Promise; - deleteMany(where: any): Promise; - healthCheck(): Promise; -} diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts deleted file mode 100644 index eb3bad0..0000000 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ /dev/null @@ -1,571 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -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; - name: string; -} - -let entityId = 2; -const entityUuid = 'uuid-'; -const entityName = 'name-'; - -const createRandomEntity = (): FakeEntity => { - const entity: FakeEntity = { - uuid: `${entityUuid}${entityId}`, - name: `${entityName}${entityId}`, - }; - - entityId++; - - return entity; -}; - -const fakeEntityToCreate: FakeEntity = { - name: 'test', -}; - -const fakeEntityCreated: FakeEntity = { - ...fakeEntityToCreate, - uuid: 'some-uuid', -}; - -const fakeEntities: FakeEntity[] = []; -Array.from({ length: 10 }).forEach(() => { - fakeEntities.push(createRandomEntity()); -}); - -@Injectable() -class FakePrismaRepository extends PrismaRepository { - protected model = 'fake'; -} - -class FakePrismaService extends PrismaService { - fake: any; -} - -const mockPrismaService = { - $transaction: jest.fn().mockImplementation(async (data: any) => { - const entities = await data[0]; - if (entities.length == 1) { - return Promise.resolve([[fakeEntityCreated], 1]); - } - - 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 Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - .mockImplementationOnce(() => { - return true; - }) - .mockImplementation(() => { - throw new Prisma.PrismaClientKnownRequestError('Database unavailable', { - code: 'code', - clientVersion: 'version', - }); - }), - fake: { - create: jest - .fn() - .mockResolvedValueOnce(fakeEntityCreated) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Error('an unknown error'); - }), - - findMany: jest.fn().mockImplementation((params?: any) => { - if (params?.where?.limit == 1) { - return Promise.resolve([fakeEntityCreated]); - } - - return Promise.resolve(fakeEntities); - }), - count: jest.fn().mockResolvedValue(fakeEntities.length), - - findUnique: jest.fn().mockImplementation(async (params?: any) => { - let entity; - - if (params?.where?.uuid) { - entity = fakeEntities.find( - (entity) => entity.uuid === params?.where?.uuid, - ); - } - - if (!entity && params?.where?.uuid == 'unknown') { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - } else if (!entity) { - throw new Error('no entity'); - } - - return entity; - }), - - findFirst: jest - .fn() - .mockImplementationOnce((params?: any) => { - if (params?.where?.name) { - return Promise.resolve( - fakeEntities.find((entity) => entity.name === params?.where?.name), - ); - } - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Error('an unknown error'); - }), - - update: jest - .fn() - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - .mockImplementationOnce((params: any) => { - const entity = fakeEntities.find( - (entity) => entity.name === params.where.name, - ); - Object.entries(params.data).map(([key, value]) => { - entity[key] = value; - }); - - return Promise.resolve(entity); - }) - .mockImplementation((params: any) => { - const entity = fakeEntities.find( - (entity) => entity.uuid === params.where.uuid, - ); - Object.entries(params.data).map(([key, value]) => { - entity[key] = value; - }); - - return Promise.resolve(entity); - }), - - delete: jest - .fn() - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - .mockImplementation((params: any) => { - let found = false; - - fakeEntities.forEach((entity, index) => { - if (entity.uuid === params?.where?.uuid) { - found = true; - fakeEntities.splice(index, 1); - } - }); - - if (!found) { - throw new Error(); - } - }), - - deleteMany: jest - .fn() - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementationOnce((params?: any) => { - throw new Prisma.PrismaClientKnownRequestError('unknown request', { - code: 'code', - clientVersion: 'version', - }); - }) - .mockImplementation((params: any) => { - let found = false; - - fakeEntities.forEach((entity, index) => { - if (entity.uuid === params?.where?.uuid) { - found = true; - fakeEntities.splice(index, 1); - } - }); - - if (!found) { - throw new Error(); - } - }), - }, -}; - -describe('PrismaRepository', () => { - let fakeRepository: FakePrismaRepository; - let prisma: FakePrismaService; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - FakePrismaRepository, - { - provide: PrismaService, - useValue: mockPrismaService, - }, - ], - }).compile(); - - fakeRepository = module.get(FakePrismaRepository); - prisma = module.get(PrismaService) as FakePrismaService; - }); - - it('should be defined', () => { - expect(fakeRepository).toBeDefined(); - expect(prisma).toBeDefined(); - }); - - describe('findAll', () => { - it('should return an array of entities', async () => { - jest.spyOn(prisma.fake, 'findMany'); - jest.spyOn(prisma.fake, 'count'); - jest.spyOn(prisma, '$transaction'); - - const entities = await fakeRepository.findAll(); - expect(entities).toStrictEqual({ - data: fakeEntities, - total: fakeEntities.length, - }); - }); - - it('should return an array containing only one entity', async () => { - const entities = await fakeRepository.findAll(1, 10, { limit: 1 }); - - expect(prisma.fake.findMany).toHaveBeenCalledWith({ - skip: 0, - take: 10, - where: { limit: 1 }, - }); - expect(entities).toEqual({ - data: [fakeEntityCreated], - total: 1, - }); - }); - }); - - describe('create', () => { - it('should create an entity', async () => { - jest.spyOn(prisma.fake, 'create'); - - const newEntity = await fakeRepository.create(fakeEntityToCreate); - expect(newEntity).toBe(fakeEntityCreated); - expect(prisma.fake.create).toHaveBeenCalledTimes(1); - }); - - it('should throw a DatabaseException for client error', async () => { - await expect( - fakeRepository.create(fakeEntityToCreate), - ).rejects.toBeInstanceOf(DatabaseException); - }); - - it('should throw a DatabaseException if uuid is not found', async () => { - await expect( - fakeRepository.create(fakeEntityToCreate), - ).rejects.toBeInstanceOf(DatabaseException); - }); - }); - - describe('findOneByUuid', () => { - it('should find an entity by uuid', async () => { - const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid); - expect(entity).toBe(fakeEntities[0]); - }); - - it('should throw a DatabaseException for client error', async () => { - await expect( - fakeRepository.findOneByUuid('unknown'), - ).rejects.toBeInstanceOf(DatabaseException); - }); - - it('should throw a DatabaseException if uuid is not found', async () => { - await expect( - fakeRepository.findOneByUuid('wrong-uuid'), - ).rejects.toBeInstanceOf(DatabaseException); - }); - }); - - describe('findOne', () => { - it('should find one entity', async () => { - const entity = await fakeRepository.findOne({ - name: fakeEntities[0].name, - }); - - expect(entity.name).toBe(fakeEntities[0].name); - }); - - it('should throw a DatabaseException for client error', async () => { - await expect( - fakeRepository.findOne({ - name: fakeEntities[0].name, - }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - - it('should throw a DatabaseException for unknown error', async () => { - await expect( - fakeRepository.findOne({ - name: fakeEntities[0].name, - }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - }); - - describe('update', () => { - it('should throw a DatabaseException for client error', async () => { - await expect( - fakeRepository.update('fake-uuid', { name: 'error' }), - ).rejects.toBeInstanceOf(DatabaseException); - await expect( - fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - - it('should update an entity with name', async () => { - const newName = 'new-random-name'; - - await fakeRepository.updateWhere( - { name: fakeEntities[0].name }, - { - name: newName, - }, - ); - expect(fakeEntities[0].name).toBe(newName); - }); - - it('should update an entity with uuid', async () => { - const newName = 'random-name'; - - await fakeRepository.update(fakeEntities[0].uuid, { - name: newName, - }); - expect(fakeEntities[0].name).toBe(newName); - }); - - it("should throw an exception if an entity doesn't exist", async () => { - await expect( - fakeRepository.update('fake-uuid', { name: 'error' }), - ).rejects.toBeInstanceOf(DatabaseException); - await expect( - fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - }); - - describe('delete', () => { - it('should throw a DatabaseException for client error', async () => { - await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf( - DatabaseException, - ); - }); - - it('should delete an entity', async () => { - const savedUuid = fakeEntities[0].uuid; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const res = await fakeRepository.delete(savedUuid); - - const deletedEntity = fakeEntities.find( - (entity) => entity.uuid === savedUuid, - ); - expect(deletedEntity).toBeUndefined(); - }); - - it("should throw an exception if an entity doesn't exist", async () => { - await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf( - DatabaseException, - ); - }); - }); - - describe('deleteMany', () => { - it('should throw a DatabaseException for client error', async () => { - await expect( - fakeRepository.deleteMany({ uuid: 'fake-uuid' }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - - it('should delete entities based on their uuid', async () => { - const savedUuid = fakeEntities[0].uuid; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const res = await fakeRepository.deleteMany({ uuid: savedUuid }); - - const deletedEntity = fakeEntities.find( - (entity) => entity.uuid === savedUuid, - ); - expect(deletedEntity).toBeUndefined(); - }); - - it("should throw an exception if an entity doesn't exist", async () => { - await expect( - fakeRepository.deleteMany({ uuid: 'fake-uuid' }), - ).rejects.toBeInstanceOf(DatabaseException); - }); - }); - - 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( - DatabaseException, - ); - }); - - it('should return a healthy result', async () => { - const res = await fakeRepository.healthCheck(); - expect(res).toBeTruthy(); - }); - - it('should throw an exception if database is not available', async () => { - await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( - DatabaseException, - ); - }); - }); -}); diff --git a/src/modules/health/adapters/primaries/health.controller.ts b/src/modules/health/adapters/primaries/health.controller.ts deleted file mode 100644 index 46a44a9..0000000 --- a/src/modules/health/adapters/primaries/health.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, Inject } from '@nestjs/common'; -import { - HealthCheckService, - HealthCheck, - HealthCheckResult, -} from '@nestjs/terminus'; -import { MESSAGE_PUBLISHER } from 'src/app.constants'; -import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase'; -import { MessagePublisherPort } from '@ports/message-publisher.port'; - -@Controller('health') -export class HealthController { - constructor( - private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase, - private healthCheckService: HealthCheckService, - @Inject(MESSAGE_PUBLISHER) - private readonly messagePublisher: MessagePublisherPort, - ) {} - - @Get() - @HealthCheck() - async check() { - try { - return await this.healthCheckService.check([ - async () => - this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'), - ]); - } catch (error) { - const healthCheckResult: HealthCheckResult = error.response; - this.messagePublisher.publish( - 'logging.user.health.crit', - JSON.stringify(healthCheckResult.error), - ); - throw error; - } - } -} diff --git a/src/modules/health/core/ports/check-repository.port.ts b/src/modules/health/core/ports/check-repository.port.ts new file mode 100644 index 0000000..64d8980 --- /dev/null +++ b/src/modules/health/core/ports/check-repository.port.ts @@ -0,0 +1,3 @@ +export interface CheckRepositoryPort { + healthCheck(): Promise; +} diff --git a/src/modules/health/core/usecases/repositories.health-indicator.usecase.ts b/src/modules/health/core/usecases/repositories.health-indicator.usecase.ts new file mode 100644 index 0000000..9600c31 --- /dev/null +++ b/src/modules/health/core/usecases/repositories.health-indicator.usecase.ts @@ -0,0 +1,48 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + HealthCheckError, + HealthCheckResult, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { CheckRepositoryPort } from '../ports/check-repository.port'; +import { AD_REPOSITORY } from '@modules/health/health.di-tokens'; +import { AdRepositoryPort } from '@modules/ad/core/ports/ad.repository.port'; +import { MESSAGE_PUBLISHER } from '@src/app.constants'; +import { MessagePublisherPort } from '@ports/message-publisher.port'; +import { LOGGING_AD_HEALTH_CRIT } from '@modules/health/health.constants'; + +@Injectable() +export class RepositoriesHealthIndicatorUseCase extends HealthIndicator { + private checkRepositories: CheckRepositoryPort[]; + constructor( + @Inject(AD_REPOSITORY) + private readonly adRepository: AdRepositoryPort, + @Inject(MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + ) { + super(); + this.checkRepositories = [adRepository]; + } + isHealthy = async (key: string): Promise => { + try { + await Promise.all( + this.checkRepositories.map( + async (checkRepository: CheckRepositoryPort) => { + await checkRepository.healthCheck(); + }, + ), + ); + return this.getStatus(key, true); + } catch (error) { + const healthCheckResult: HealthCheckResult = error; + this.messagePublisher.publish( + LOGGING_AD_HEALTH_CRIT, + JSON.stringify(healthCheckResult.error), + ); + throw new HealthCheckError('Repository', { + repository: error.message, + }); + } + }; +} diff --git a/src/modules/health/domain/interfaces/check-repository.interface.ts b/src/modules/health/domain/interfaces/check-repository.interface.ts deleted file mode 100644 index 68c3178..0000000 --- a/src/modules/health/domain/interfaces/check-repository.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ICheckRepository { - healthCheck(): Promise; -} diff --git a/src/modules/health/domain/usecases/repositories.health-indicator.usecase.ts b/src/modules/health/domain/usecases/repositories.health-indicator.usecase.ts deleted file mode 100644 index 1a6ef47..0000000 --- a/src/modules/health/domain/usecases/repositories.health-indicator.usecase.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - HealthCheckError, - HealthIndicator, - HealthIndicatorResult, -} from '@nestjs/terminus'; -import { ICheckRepository } from '../interfaces/check-repository.interface'; -import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; - -@Injectable() -export class RepositoriesHealthIndicatorUseCase extends HealthIndicator { - private checkRepositories: ICheckRepository[]; - constructor(private readonly adRepository: AdRepository) { - super(); - this.checkRepositories = [adRepository]; - } - isHealthy = async (key: string): Promise => { - try { - await Promise.all( - this.checkRepositories.map( - async (checkRepository: ICheckRepository) => { - await checkRepository.healthCheck(); - }, - ), - ); - return this.getStatus(key, true); - } catch (e: any) { - throw new HealthCheckError('Repository', { - repository: e.message, - }); - } - }; -} diff --git a/src/modules/health/health.constants.ts b/src/modules/health/health.constants.ts new file mode 100644 index 0000000..3f29432 --- /dev/null +++ b/src/modules/health/health.constants.ts @@ -0,0 +1 @@ +export const LOGGING_AD_HEALTH_CRIT = 'logging.ad.health.crit'; diff --git a/src/modules/health/health.di-tokens.ts b/src/modules/health/health.di-tokens.ts new file mode 100644 index 0000000..2706306 --- /dev/null +++ b/src/modules/health/health.di-tokens.ts @@ -0,0 +1 @@ +export const AD_REPOSITORY = Symbol('AD_REPOSITORY'); diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index d5ef060..945c281 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,20 +1,23 @@ import { Module } from '@nestjs/common'; -import { HealthServerController } from './adapters/primaries/health-server.controller'; -import { DatabaseModule } from '../database/database.module'; -import { HealthController } from './adapters/primaries/health.controller'; +import { HealthHttpController } from './interface/http-controllers/health.http.controller'; import { TerminusModule } from '@nestjs/terminus'; import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; -import { MessagePublisher } from './adapters/secondaries/message-publisher'; -import { RepositoriesHealthIndicatorUseCase } from './domain/usecases/repositories.health-indicator.usecase'; +import { MessagePublisher } from './infrastructure/message-publisher'; +import { RepositoriesHealthIndicatorUseCase } from './core/usecases/repositories.health-indicator.usecase'; import { AdRepository } from '../ad/infrastructure/ad.repository'; +import { AD_REPOSITORY } from './health.di-tokens'; +import { HealthGrpcController } from './interface/grpc-controllers/health.grpc.controller'; @Module({ - imports: [TerminusModule, DatabaseModule], - controllers: [HealthServerController, HealthController], + imports: [TerminusModule], + controllers: [HealthGrpcController, HealthHttpController], providers: [ RepositoriesHealthIndicatorUseCase, - AdRepository, + { + provide: AD_REPOSITORY, + useClass: AdRepository, + }, { provide: MESSAGE_BROKER_PUBLISHER, useClass: MessageBrokerPublisher, diff --git a/src/modules/health/adapters/secondaries/message-publisher.ts b/src/modules/health/infrastructure/message-publisher.ts similarity index 88% rename from src/modules/health/adapters/secondaries/message-publisher.ts rename to src/modules/health/infrastructure/message-publisher.ts index 1350413..070f5b1 100644 --- a/src/modules/health/adapters/secondaries/message-publisher.ts +++ b/src/modules/health/infrastructure/message-publisher.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants'; +import { MESSAGE_BROKER_PUBLISHER } from '../../../app.constants'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { MessagePublisherPort } from '@ports/message-publisher.port'; diff --git a/src/modules/health/adapters/primaries/health-server.controller.ts b/src/modules/health/interface/grpc-controllers/health.grpc.controller.ts similarity index 86% rename from src/modules/health/adapters/primaries/health-server.controller.ts rename to src/modules/health/interface/grpc-controllers/health.grpc.controller.ts index 3cdc70d..5aa260e 100644 --- a/src/modules/health/adapters/primaries/health-server.controller.ts +++ b/src/modules/health/interface/grpc-controllers/health.grpc.controller.ts @@ -1,6 +1,6 @@ import { Controller } from '@nestjs/common'; import { GrpcMethod } from '@nestjs/microservices'; -import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase'; +import { RepositoriesHealthIndicatorUseCase } from '../../core/usecases/repositories.health-indicator.usecase'; enum ServingStatus { UNKNOWN = 0, @@ -17,7 +17,7 @@ interface HealthCheckResponse { } @Controller() -export class HealthServerController { +export class HealthGrpcController { constructor( private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase, ) {} diff --git a/src/modules/health/adapters/primaries/health.proto b/src/modules/health/interface/grpc-controllers/health.proto similarity index 100% rename from src/modules/health/adapters/primaries/health.proto rename to src/modules/health/interface/grpc-controllers/health.proto diff --git a/src/modules/health/interface/http-controllers/health.http.controller.ts b/src/modules/health/interface/http-controllers/health.http.controller.ts new file mode 100644 index 0000000..0e43fee --- /dev/null +++ b/src/modules/health/interface/http-controllers/health.http.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthCheckService, HealthCheck } from '@nestjs/terminus'; +import { RepositoriesHealthIndicatorUseCase } from '../../core/usecases/repositories.health-indicator.usecase'; + +@Controller('health') +export class HealthHttpController { + constructor( + private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase, + private healthCheckService: HealthCheckService, + ) {} + + @Get() + @HealthCheck() + async check() { + try { + return await this.healthCheckService.check([ + async () => + this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'), + ]); + } catch (error) { + throw error; + } + } +} diff --git a/src/modules/health/tests/unit/message-publisher.spec.ts b/src/modules/health/tests/unit/message-publisher.spec.ts index eec02ea..7b3e9a9 100644 --- a/src/modules/health/tests/unit/message-publisher.spec.ts +++ b/src/modules/health/tests/unit/message-publisher.spec.ts @@ -1,6 +1,6 @@ +import { MessagePublisher } from '@modules/health/infrastructure/message-publisher'; import { Test, TestingModule } from '@nestjs/testing'; -import { MessagePublisher } from '../../adapters/secondaries/message-publisher'; -import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants'; +import { MESSAGE_BROKER_PUBLISHER } from '@src/app.constants'; const mockMessageBrokerPublisher = { publish: jest.fn().mockImplementation(), diff --git a/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts index 0353505..6e5642f 100644 --- a/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts +++ b/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts @@ -1,19 +1,25 @@ import { Test, TestingModule } from '@nestjs/testing'; import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; -import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase'; -import { AdsRepository } from '../../../ad/adapters/secondaries/ads.repository'; +import { RepositoriesHealthIndicatorUseCase } from '../../core/usecases/repositories.health-indicator.usecase'; +import { AD_REPOSITORY } from '@modules/health/health.di-tokens'; +import { MESSAGE_PUBLISHER } from '@src/app.constants'; +import { DatabaseErrorException } from '@libs/exceptions'; -const mockAdsRepository = { +const mockAdRepository = { healthCheck: jest .fn() .mockImplementationOnce(() => { return Promise.resolve(true); }) .mockImplementation(() => { - throw new Error('an error occured in the repository'); + throw new DatabaseErrorException('an error occured in the database'); }), }; +const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), +}; + describe('RepositoriesHealthIndicatorUseCase', () => { let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase; @@ -22,8 +28,12 @@ describe('RepositoriesHealthIndicatorUseCase', () => { providers: [ RepositoriesHealthIndicatorUseCase, { - provide: AdsRepository, - useValue: mockAdsRepository, + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + { + provide: MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, }, ], }).compile(); @@ -42,7 +52,6 @@ describe('RepositoriesHealthIndicatorUseCase', () => { it('should check health successfully', async () => { const healthIndicatorResult: HealthIndicatorResult = await repositoriesHealthIndicatorUseCase.isHealthy('repositories'); - expect(healthIndicatorResult['repositories'].status).toBe('up'); }); diff --git a/tsconfig.json b/tsconfig.json index a1f3fca..c42aa05 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "@modules/*": ["src/modules/*"], "@ports/*": ["src/ports/*"], "@utils/*": ["src/utils/*"], + "@src/*": ["src/*"], } } }