refactor to ddh, first commit

This commit is contained in:
sbriat 2023-08-16 12:28:20 +02:00
parent 0a6e4c0bf6
commit ce48890a66
208 changed files with 2596 additions and 2052 deletions

View File

@ -4,6 +4,21 @@ SERVICE_PORT=5005
SERVICE_CONFIGURATION_DOMAIN=MATCHER SERVICE_CONFIGURATION_DOMAIN=MATCHER
HEALTH_SERVICE_PORT=6005 HEALTH_SERVICE_PORT=6005
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# REDIS
REDIS_HOST=v3-redis
REDIS_PASSWORD=redis
REDIS_PORT=6379
# CACHE
CACHE_TTL=5000
# DEFAULT CONFIGURATION # DEFAULT CONFIGURATION
# default identifier used for match requests # default identifier used for match requests
@ -41,18 +56,3 @@ MAX_DETOUR_DURATION_RATIO=0.3
GEOROUTER_TYPE=graphhopper GEOROUTER_TYPE=graphhopper
# georouter url # georouter url
GEOROUTER_URL=http://localhost:8989 GEOROUTER_URL=http://localhost:8989
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
# RABBIT MQ
RMQ_URI=amqp://v3-broker:5672
RMQ_EXCHANGE=mobicoop
# REDIS
REDIS_HOST=v3-redis
REDIS_PASSWORD=redis
REDIS_PORT=6379
# CACHE
CACHE_TTL=5000

View File

@ -44,8 +44,9 @@ GEOROUTER_URL=http://localhost:8989
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
# RABBIT MQ # MESSAGE BROKER
RMQ_URI=amqp://v3-broker:5672 MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# REDIS # REDIS
REDIS_IMAGE=redis:7.0-alpine REDIS_IMAGE=redis:7.0-alpine

2
old/app.constants.ts Normal file
View File

@ -0,0 +1,2 @@
export const MESSAGE_BROKER_PUBLISHER = Symbol('MESSAGE_BROKER_PUBLISHER');
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');

66
old/app.module.ts Normal file
View File

@ -0,0 +1,66 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HealthModule } from './modules/health/health.module';
import { MatcherModule } from './modules/matcher/matcher.module';
import { AdModule } from './modules/ad/ad.module';
import {
ConfigurationModule,
ConfigurationModuleOptions,
} from '@mobicoop/configuration-module';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ConfigurationModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<ConfigurationModuleOptions> => ({
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
messageBroker: {
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
},
redis: {
host: configService.get<string>('REDIS_HOST'),
password: configService.get<string>('REDIS_PASSWORD'),
port: configService.get<number>('REDIS_PORT'),
},
setConfigurationBrokerQueue: 'matcher-configuration-create-update',
deleteConfigurationQueue: 'matcher-configuration-delete',
propagateConfigurationQueue: 'matcher-configuration-propagate',
}),
}),
MessageBrokerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<MessageBrokerModuleOptions> => ({
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
handlers: {
adCreated: {
routingKey: 'ad.created',
queue: 'matcher-ad-created',
},
},
name: 'matcher',
}),
}),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
HealthModule,
MatcherModule,
AdModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

View File

@ -0,0 +1,3 @@
export interface IPublishMessage {
publish(routingKey: string, message: string): void;
}

View File

@ -0,0 +1,4 @@
export const PARAMS_PROVIDER = Symbol();
export const GEOROUTER_CREATOR = Symbol();
export const TIMEZONE_FINDER = Symbol();
export const DIRECTION_ENCODER = Symbol();

View File

@ -0,0 +1,61 @@
import { Module } from '@nestjs/common';
import { AdMessagerService } from './adapters/primaries/ad-messager.service';
import { AdProfile } from './mappers/ad.profile';
import { CreateAdUseCase } from './domain/usecases/create-ad.usecase';
import { AdRepository } from './adapters/secondaries/ad.repository';
import { DatabaseModule } from '../database/database.module';
import { CqrsModule } from '@nestjs/cqrs';
import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder';
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
import { GeorouterCreator } from '../geography/adapters/secondaries/georouter-creator';
import { GeographyModule } from '../geography/geography.module';
import { HttpModule } from '@nestjs/axios';
import { PostgresDirectionEncoder } from '../geography/adapters/secondaries/postgres-direction-encoder';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import {
MESSAGE_BROKER_PUBLISHER,
MESSAGE_PUBLISHER,
} from '../../app.constants';
import { MessagePublisher } from './adapters/secondaries/message-publisher';
import {
DIRECTION_ENCODER,
GEOROUTER_CREATOR,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
} from './ad.constants';
@Module({
imports: [GeographyModule, DatabaseModule, CqrsModule, HttpModule],
providers: [
{
provide: PARAMS_PROVIDER,
useClass: DefaultParamsProvider,
},
{
provide: GEOROUTER_CREATOR,
useClass: GeorouterCreator,
},
{
provide: TIMEZONE_FINDER,
useClass: GeoTimezoneFinder,
},
{
provide: DIRECTION_ENCODER,
useClass: PostgresDirectionEncoder,
},
{
provide: MESSAGE_BROKER_PUBLISHER,
useClass: MessageBrokerPublisher,
},
{
provide: MESSAGE_PUBLISHER,
useClass: MessagePublisher,
},
AdProfile,
AdRepository,
CreateAdUseCase,
AdMessagerService,
],
exports: [],
})
export class AdModule {}

View File

@ -1,20 +1,21 @@
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { Controller, Inject } from '@nestjs/common';
import { Controller } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdCommand } from '../../commands/create-ad.command';
import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
import { validateOrReject } from 'class-validator'; import { validateOrReject } from 'class-validator';
import { Messager } from '../secondaries/messager';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { DatabaseException } from 'src/modules/database/exceptions/database.exception'; import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
import { ExceptionCode } from 'src/modules/utils/exception-code.enum'; import { ExceptionCode } from 'src/modules/utils/exception-code.enum';
import { IPublishMessage } from 'src/interfaces/message-publisher';
import { MESSAGE_PUBLISHER } from 'src/app.constants';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
@Controller() @Controller()
export class AdMessagerController { export class AdMessagerService {
constructor( constructor(
private readonly messager: Messager, @Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
private readonly commandBus: CommandBus, private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {} ) {}
@RabbitSubscribe({ @RabbitSubscribe({
@ -28,7 +29,7 @@ export class AdMessagerController {
// validate instance // validate instance
await validateOrReject(createAdRequest); await validateOrReject(createAdRequest);
// validate nested objects (fixes direct nested validation bug) // validate nested objects (fixes direct nested validation bug)
for (const waypoint of createAdRequest.waypoints) { for (const waypoint of createAdRequest.addresses) {
try { try {
await validateOrReject(waypoint); await validateOrReject(waypoint);
} catch (e) { } catch (e) {
@ -36,7 +37,7 @@ export class AdMessagerController {
} }
} }
} catch (e) { } catch (e) {
this.messager.publish( this.messagePublisher.publish(
'matcher.ad.crit', 'matcher.ad.crit',
JSON.stringify({ JSON.stringify({
message: `Can't validate message : ${message}`, message: `Can't validate message : ${message}`,
@ -49,7 +50,7 @@ export class AdMessagerController {
} catch (e) { } catch (e) {
if (e instanceof DatabaseException) { if (e instanceof DatabaseException) {
if (e.message.includes('already exists')) { if (e.message.includes('already exists')) {
this.messager.publish( this.messagePublisher.publish(
'matcher.ad.crit', 'matcher.ad.crit',
JSON.stringify({ JSON.stringify({
code: ExceptionCode.ALREADY_EXISTS, code: ExceptionCode.ALREADY_EXISTS,
@ -59,7 +60,7 @@ export class AdMessagerController {
); );
} }
if (e.message.includes("Can't reach database server")) { if (e.message.includes("Can't reach database server")) {
this.messager.publish( this.messagePublisher.publish(
'matcher.ad.crit', 'matcher.ad.crit',
JSON.stringify({ JSON.stringify({
code: ExceptionCode.UNAVAILABLE, code: ExceptionCode.UNAVAILABLE,
@ -69,7 +70,7 @@ export class AdMessagerController {
); );
} }
} }
this.messager.publish( this.messagePublisher.publish(
'logging.matcher.ad.crit', 'logging.matcher.ad.crit',
JSON.stringify({ JSON.stringify({
message, message,

View File

@ -0,0 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@Injectable()
export class MessagePublisher implements IPublishMessage {
constructor(
@Inject(MESSAGE_BROKER_PUBLISHER)
private readonly messageBrokerPublisher: MessageBrokerPublisher,
) {}
publish = (routingKey: string, message: string): void => {
this.messageBrokerPublisher.publish(routingKey, message);
};
}

View File

@ -119,7 +119,7 @@ export class CreateAdRequest {
@IsArray() @IsArray()
@ArrayMinSize(2) @ArrayMinSize(2)
@AutoMap(() => [Coordinate]) @AutoMap(() => [Coordinate])
waypoints: Coordinate[]; addresses: Coordinate[];
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()

View File

@ -21,22 +21,22 @@ export class Geography {
): Promise<void> => { ): Promise<void> => {
const paths: Path[] = this.getPaths(roles); const paths: Path[] = this.getPaths(roles);
const routes = await georouter.route(paths, settings); const routes = await georouter.route(paths, settings);
if (routes.some((route) => route.key == RouteKey.COMMON)) { if (routes.some((route) => route.key == RouteType.COMMON)) {
this.driverRoute = routes.find( this.driverRoute = routes.find(
(route) => route.key == RouteKey.COMMON, (route) => route.key == RouteType.COMMON,
).route; ).route;
this.passengerRoute = routes.find( this.passengerRoute = routes.find(
(route) => route.key == RouteKey.COMMON, (route) => route.key == RouteType.COMMON,
).route; ).route;
} else { } else {
if (routes.some((route) => route.key == RouteKey.DRIVER)) { if (routes.some((route) => route.key == RouteType.DRIVER)) {
this.driverRoute = routes.find( this.driverRoute = routes.find(
(route) => route.key == RouteKey.DRIVER, (route) => route.key == RouteType.DRIVER,
).route; ).route;
} }
if (routes.some((route) => route.key == RouteKey.PASSENGER)) { if (routes.some((route) => route.key == RouteType.PASSENGER)) {
this.passengerRoute = routes.find( this.passengerRoute = routes.find(
(route) => route.key == RouteKey.PASSENGER, (route) => route.key == RouteType.PASSENGER,
).route; ).route;
} }
} }
@ -48,7 +48,7 @@ export class Geography {
if (this.coordinates.length == 2) { if (this.coordinates.length == 2) {
// 2 points => same route for driver and passenger // 2 points => same route for driver and passenger
const commonPath: Path = { const commonPath: Path = {
key: RouteKey.COMMON, key: RouteType.COMMON,
points: this.coordinates, points: this.coordinates,
}; };
paths.push(commonPath); paths.push(commonPath);
@ -69,14 +69,14 @@ export class Geography {
private createDriverPath = (): Path => { private createDriverPath = (): Path => {
return { return {
key: RouteKey.DRIVER, key: RouteType.DRIVER,
points: this.coordinates, points: this.coordinates,
}; };
}; };
private createPassengerPath = (): Path => { private createPassengerPath = (): Path => {
return { return {
key: RouteKey.PASSENGER, key: RouteType.PASSENGER,
points: [ points: [
this.coordinates[0], this.coordinates[0],
this.coordinates[this.coordinates.length - 1], this.coordinates[this.coordinates.length - 1],
@ -85,7 +85,7 @@ export class Geography {
}; };
} }
export enum RouteKey { export enum RouteType {
COMMON = 'common', COMMON = 'common',
DRIVER = 'driver', DRIVER = 'driver',
PASSENGER = 'passenger', PASSENGER = 'passenger',

View File

@ -16,6 +16,12 @@ import { Geography } from '../entities/geography';
import { IEncodeDirection } from '../../../geography/domain/interfaces/direction-encoder.interface'; import { IEncodeDirection } from '../../../geography/domain/interfaces/direction-encoder.interface';
import { TimeConverter } from '../entities/time-converter'; import { TimeConverter } from '../entities/time-converter';
import { Coordinate } from '../../../geography/domain/entities/coordinate'; import { Coordinate } from '../../../geography/domain/entities/coordinate';
import {
DIRECTION_ENCODER,
GEOROUTER_CREATOR,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
} from '../../ad.constants';
@CommandHandler(CreateAdCommand) @CommandHandler(CreateAdCommand)
export class CreateAdUseCase { export class CreateAdUseCase {
@ -29,13 +35,13 @@ export class CreateAdUseCase {
constructor( constructor(
@InjectMapper() private readonly mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
private readonly adRepository: AdRepository, private readonly adRepository: AdRepository,
@Inject('ParamsProvider') @Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: IProvideParams, private readonly defaultParamsProvider: IProvideParams,
@Inject('GeorouterCreator') @Inject(GEOROUTER_CREATOR)
private readonly georouterCreator: ICreateGeorouter, private readonly georouterCreator: ICreateGeorouter,
@Inject('TimezoneFinder') @Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: IFindTimezone, private readonly timezoneFinder: IFindTimezone,
@Inject('DirectionEncoder') @Inject(DIRECTION_ENCODER)
private readonly directionEncoder: IEncodeDirection, private readonly directionEncoder: IEncodeDirection,
) { ) {
this.defaultParams = defaultParamsProvider.getParams(); this.defaultParams = defaultParamsProvider.getParams();
@ -48,8 +54,8 @@ export class CreateAdUseCase {
async execute(command: CreateAdCommand): Promise<Ad> { async execute(command: CreateAdCommand): Promise<Ad> {
try { try {
this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad); this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad);
this.setTimezone(command.createAdRequest.waypoints); this.setTimezone(command.createAdRequest.addresses);
this.setGeography(command.createAdRequest.waypoints); this.setGeography(command.createAdRequest.addresses);
this.setRoles(command.createAdRequest); this.setRoles(command.createAdRequest);
await this.geography.createRoutes(this.roles, this.georouter, { await this.geography.createRoutes(this.roles, this.georouter, {
withDistance: false, withDistance: false,
@ -97,7 +103,7 @@ export class CreateAdUseCase {
? this.geography.driverRoute.backAzimuth ? this.geography.driverRoute.backAzimuth
: this.geography.passengerRoute.backAzimuth; : this.geography.passengerRoute.backAzimuth;
this.ad.waypoints = this.directionEncoder.encode( this.ad.waypoints = this.directionEncoder.encode(
command.createAdRequest.waypoints, command.createAdRequest.addresses,
); );
this.ad.direction = this.geography.driverRoute this.ad.direction = this.geography.driverRoute
? this.directionEncoder.encode(this.geography.driverRoute.points) ? this.directionEncoder.encode(this.geography.driverRoute.points)

View File

@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MessagePublisher } from '../../../../adapters/secondaries/message-publisher';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../../../app.constants';
const mockMessageBrokerPublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Messager', () => {
let messagePublisher: MessagePublisher;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
MessagePublisher,
{
provide: MESSAGE_BROKER_PUBLISHER,
useValue: mockMessageBrokerPublisher,
},
],
}).compile();
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
});
it('should be defined', () => {
expect(messagePublisher).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockMessageBrokerPublisher, 'publish');
messagePublisher.publish('ad.info', 'my-test');
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -8,9 +8,15 @@ import { CreateAdCommand } from '../../../commands/create-ad.command';
import { Ad } from '../../../domain/entities/ad'; import { Ad } from '../../../domain/entities/ad';
import { AdProfile } from '../../../mappers/ad.profile'; import { AdProfile } from '../../../mappers/ad.profile';
import { Frequency } from '../../../domain/types/frequency.enum'; import { Frequency } from '../../../domain/types/frequency.enum';
import { RouteKey } from '../../../domain/entities/geography'; import { RouteType } from '../../../domain/entities/geography';
import { DatabaseException } from '../../../../database/exceptions/database.exception'; import { DatabaseException } from '../../../../database/exceptions/database.exception';
import { Route } from '../../../../geography/domain/entities/route'; import { Route } from '../../../../geography/domain/entities/route';
import {
DIRECTION_ENCODER,
GEOROUTER_CREATOR,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
} from '../../../ad.constants';
const mockAdRepository = { const mockAdRepository = {
createAd: jest.fn().mockImplementation((ad) => { createAd: jest.fn().mockImplementation((ad) => {
@ -23,7 +29,7 @@ const mockGeorouterCreator = {
create: jest.fn().mockImplementation(() => ({ create: jest.fn().mockImplementation(() => ({
route: jest.fn().mockImplementation(() => [ route: jest.fn().mockImplementation(() => [
{ {
key: RouteKey.DRIVER, key: RouteType.DRIVER,
route: <Route>{ route: <Route>{
points: [], points: [],
fwdAzimuth: 0, fwdAzimuth: 0,
@ -33,7 +39,7 @@ const mockGeorouterCreator = {
}, },
}, },
{ {
key: RouteKey.PASSENGER, key: RouteType.PASSENGER,
route: <Route>{ route: <Route>{
points: [], points: [],
fwdAzimuth: 0, fwdAzimuth: 0,
@ -43,7 +49,7 @@ const mockGeorouterCreator = {
}, },
}, },
{ {
key: RouteKey.COMMON, key: RouteType.COMMON,
route: <Route>{ route: <Route>{
points: [], points: [],
fwdAzimuth: 0, fwdAzimuth: 0,
@ -94,7 +100,7 @@ const createAdRequest: CreateAdRequest = {
seatsDriver: 3, seatsDriver: 3,
seatsPassenger: 1, seatsPassenger: 1,
strict: false, strict: false,
waypoints: [ addresses: [
{ lon: 6, lat: 45 }, { lon: 6, lat: 45 },
{ lon: 6.5, lat: 45.5 }, { lon: 6.5, lat: 45.5 },
], ],
@ -124,19 +130,19 @@ describe('CreateAdUseCase', () => {
useValue: mockAdRepository, useValue: mockAdRepository,
}, },
{ {
provide: 'GeorouterCreator', provide: GEOROUTER_CREATOR,
useValue: mockGeorouterCreator, useValue: mockGeorouterCreator,
}, },
{ {
provide: 'ParamsProvider', provide: PARAMS_PROVIDER,
useValue: mockParamsProvider, useValue: mockParamsProvider,
}, },
{ {
provide: 'TimezoneFinder', provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder, useValue: mockTimezoneFinder,
}, },
{ {
provide: 'DirectionEncoder', provide: DIRECTION_ENCODER,
useValue: mockDirectionEncoder, useValue: mockDirectionEncoder,
}, },
AdProfile, AdProfile,

View File

@ -5,7 +5,7 @@ import { GraphhopperGeorouter } from './graphhopper-georouter';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { Geodesic } from './geodesic'; import { Geodesic } from './geodesic';
import { GeographyException } from '../../exceptions/geography.exception'; import { GeographyException } from '../../exceptions/geography.exception';
import { ExceptionCode } from '../../..//utils/exception-code.enum'; import { ExceptionCode } from '../../../utils/exception-code.enum';
@Injectable() @Injectable()
export class GeorouterCreator implements ICreateGeorouter { export class GeorouterCreator implements ICreateGeorouter {

View File

@ -3,12 +3,12 @@ import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { catchError, lastValueFrom, map } from 'rxjs'; import { catchError, lastValueFrom, map } from 'rxjs';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
import { Path } from '../../domain/types/path.type'; import { Path } from '../../domain/types/path.type';
import { NamedRoute } from '../../domain/types/named-route'; import { NamedRoute } from '../../domain/types/named-route';
import { GeographyException } from '../../exceptions/geography.exception'; import { GeographyException } from '../../exceptions/geography.exception';
import { ExceptionCode } from '../../..//utils/exception-code.enum'; import { ExceptionCode } from '../../../utils/exception-code.enum';
import { Route } from '../../domain/entities/route'; import { Route } from '../../domain/entities/route';
import { SpacetimePoint } from '../../domain/entities/spacetime-point'; import { SpacetimePoint } from '../../domain/entities/spacetime-point';

View File

@ -0,0 +1,6 @@
import { Point } from './point.type';
export type Path = {
key: string;
points: Point[];
};

View File

@ -1,6 +1,6 @@
import { Controller } from '@nestjs/common'; import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices'; import { GrpcMethod } from '@nestjs/microservices';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
enum ServingStatus { enum ServingStatus {
UNKNOWN = 0, UNKNOWN = 0,
@ -19,7 +19,7 @@ interface HealthCheckResponse {
@Controller() @Controller()
export class HealthServerController { export class HealthServerController {
constructor( constructor(
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
) {} ) {}
@GrpcMethod('Health', 'Check') @GrpcMethod('Health', 'Check')
@ -29,12 +29,12 @@ export class HealthServerController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
metadata: any, metadata: any,
): Promise<HealthCheckResponse> { ): Promise<HealthCheckResponse> {
const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy( const healthCheck = await this.repositoriesHealthIndicatorUseCase.isHealthy(
'prisma', 'repositories',
); );
return { return {
status: status:
healthCheck['prisma'].status == 'up' healthCheck['repositories'].status == 'up'
? ServingStatus.SERVING ? ServingStatus.SERVING
: ServingStatus.NOT_SERVING, : ServingStatus.NOT_SERVING,
}; };

View File

@ -0,0 +1,37 @@
import { Controller, Get, Inject } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
import { MESSAGE_PUBLISHER } from 'src/app.constants';
import { IPublishMessage } from 'src/interfaces/message-publisher';
import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
@Controller('health')
export class HealthController {
constructor(
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
private healthCheckService: HealthCheckService,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
) {}
@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;
}
}
}

View File

@ -0,0 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { IPublishMessage } from 'src/interfaces/message-publisher';
@Injectable()
export class MessagePublisher implements IPublishMessage {
constructor(
@Inject(MESSAGE_BROKER_PUBLISHER)
private readonly messageBrokerPublisher: MessageBrokerPublisher,
) {}
publish = (routingKey: string, message: string): void => {
this.messageBrokerPublisher.publish(routingKey, message);
};
}

View File

@ -0,0 +1,3 @@
export interface ICheckRepository {
healthCheck(): Promise<boolean>;
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { ICheckRepository } from '../interfaces/check-repository.interface';
import { AdRepository } from '../../../ad/adapters/secondaries/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,
});
}
};
}

View File

@ -0,0 +1,28 @@
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 { 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 { AdRepository } from '../ad/adapters/secondaries/ad.repository';
@Module({
imports: [TerminusModule, DatabaseModule],
controllers: [HealthServerController, HealthController],
providers: [
RepositoriesHealthIndicatorUseCase,
AdRepository,
{
provide: MESSAGE_BROKER_PUBLISHER,
useClass: MessageBrokerPublisher,
},
{
provide: MESSAGE_PUBLISHER,
useClass: MessagePublisher,
},
],
})
export class HealthModule {}

View File

@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
const mockMessageBrokerPublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Messager', () => {
let messagePublisher: MessagePublisher;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
MessagePublisher,
{
provide: MESSAGE_BROKER_PUBLISHER,
useValue: mockMessageBrokerPublisher,
},
],
}).compile();
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
});
it('should be defined', () => {
expect(messagePublisher).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockMessageBrokerPublisher, 'publish');
messagePublisher.publish('health.info', 'my-test');
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,8 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository'; import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
const mockAdRepository = { const mockAdRepository = {
healthCheck: jest healthCheck: jest
@ -11,47 +10,45 @@ const mockAdRepository = {
return Promise.resolve(true); return Promise.resolve(true);
}) })
.mockImplementation(() => { .mockImplementation(() => {
throw new PrismaClientKnownRequestError('Service unavailable', { throw new Error('an error occured in the repository');
code: 'code',
clientVersion: 'version',
});
}), }),
}; };
describe('PrismaHealthIndicatorUseCase', () => { describe('RepositoriesHealthIndicatorUseCase', () => {
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase; let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase;
beforeAll(async () => { beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
RepositoriesHealthIndicatorUseCase,
{ {
provide: AdRepository, provide: AdRepository,
useValue: mockAdRepository, useValue: mockAdRepository,
}, },
PrismaHealthIndicatorUseCase,
], ],
}).compile(); }).compile();
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>( repositoriesHealthIndicatorUseCase =
PrismaHealthIndicatorUseCase, module.get<RepositoriesHealthIndicatorUseCase>(
RepositoriesHealthIndicatorUseCase,
); );
}); });
it('should be defined', () => { it('should be defined', () => {
expect(prismaHealthIndicatorUseCase).toBeDefined(); expect(repositoriesHealthIndicatorUseCase).toBeDefined();
}); });
describe('execute', () => { describe('execute', () => {
it('should check health successfully', async () => { it('should check health successfully', async () => {
const healthIndicatorResult: HealthIndicatorResult = const healthIndicatorResult: HealthIndicatorResult =
await prismaHealthIndicatorUseCase.isHealthy('prisma'); await repositoriesHealthIndicatorUseCase.isHealthy('repositories');
expect(healthIndicatorResult['prisma'].status).toBe('up'); expect(healthIndicatorResult['repositories'].status).toBe('up');
}); });
it('should throw an error if database is unavailable', async () => { it('should throw an error if database is unavailable', async () => {
await expect( await expect(
prismaHealthIndicatorUseCase.isHealthy('prisma'), repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
).rejects.toBeInstanceOf(HealthCheckError); ).rejects.toBeInstanceOf(HealthCheckError);
}); });
}); });

View File

@ -0,0 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@Injectable()
export class MessagePublisher implements IPublishMessage {
constructor(
@Inject(MESSAGE_BROKER_PUBLISHER)
private readonly messageBrokerPublisher: MessageBrokerPublisher,
) {}
publish = (routingKey: string, message: string): void => {
this.messageBrokerPublisher.publish(routingKey, message);
};
}

Some files were not shown because too many files have changed in this diff Show More