working version, with basic tests
This commit is contained in:
		
							parent
							
								
									e1989c0a52
								
							
						
					
					
						commit
						b232247c93
					
				
							
								
								
									
										21
									
								
								package.json
								
								
								
								
							
							
						
						
									
										21
									
								
								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(.*)": "<rootDir>/libs/$1",
 | 
			
		||||
      "^@modules(.*)": "<rootDir>/modules/$1"
 | 
			
		||||
      "^@modules(.*)": "<rootDir>/modules/$1",
 | 
			
		||||
      "^@src(.*)": "<rootDir>$1"
 | 
			
		||||
    },
 | 
			
		||||
    "testEnvironment": "node"
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<any>,
 | 
			
		||||
| 
						 | 
				
			
			@ -17,16 +19,11 @@ export abstract class PrismaRepositoryBase<
 | 
			
		|||
    protected readonly logger: LoggerPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async findOneById(uuid: string): Promise<Option<Aggregate>> {
 | 
			
		||||
    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<Option<Aggregate>> {
 | 
			
		||||
    const entity = await this.prisma.findUnique({
 | 
			
		||||
      where: { uuid: id },
 | 
			
		||||
    });
 | 
			
		||||
    return entity ? Some(this.mapper.toDomain(entity)) : None;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async insert(entity: Aggregate): Promise<void> {
 | 
			
		||||
| 
						 | 
				
			
			@ -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<boolean> {
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<AggregateID, AdAlreadyExistsError> =
 | 
			
		||||
        await createAdService.execute(createAdCommand);
 | 
			
		||||
      expect(result.unwrap()).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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<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 Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.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 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<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();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.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 Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.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 Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.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 Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findAllByQuery(
 | 
			
		||||
    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,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createWithFields(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();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateWithFields(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();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async healthCheck(): 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();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
 | 
			
		||||
 | 
			
		||||
export class AdRepository<T> extends PrismaRepository<T> {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export interface ICollection<T> {
 | 
			
		||||
  data: T[];
 | 
			
		||||
  total: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { ICollection } from './collection.interface';
 | 
			
		||||
 | 
			
		||||
export interface IRepository<T> {
 | 
			
		||||
  findAll(
 | 
			
		||||
    page: number,
 | 
			
		||||
    perPage: number,
 | 
			
		||||
    params?: any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<ICollection<T>>;
 | 
			
		||||
  findOne(where: any, include?: any): Promise<T>;
 | 
			
		||||
  findOneByUuid(uuid: string, include?: any): Promise<T>;
 | 
			
		||||
  create(entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
 | 
			
		||||
  updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  delete(uuid: string): Promise<T>;
 | 
			
		||||
  deleteMany(where: any): Promise<void>;
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<FakeEntity> {
 | 
			
		||||
  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>(FakePrismaRepository);
 | 
			
		||||
    prisma = module.get<PrismaService>(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,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export interface CheckRepositoryPort {
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<HealthIndicatorResult> => {
 | 
			
		||||
    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,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export interface ICheckRepository {
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<HealthIndicatorResult> => {
 | 
			
		||||
    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,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export const LOGGING_AD_HEALTH_CRIT = 'logging.ad.health.crit';
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  ) {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@
 | 
			
		|||
      "@modules/*": ["src/modules/*"],
 | 
			
		||||
      "@ports/*": ["src/ports/*"],
 | 
			
		||||
      "@utils/*": ["src/utils/*"],
 | 
			
		||||
      "@src/*": ["src/*"],
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue