Merge branch 'healthUpgrade' into 'main'

upgrade health, use configuration and broker packages

See merge request v3/service/ad!7
This commit is contained in:
Sylvain Briat 2023-06-06 10:57:39 +00:00
commit cd84567107
47 changed files with 540 additions and 779 deletions

View File

@ -7,9 +7,9 @@ HEALTH_SERVICE_PORT=6006
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=ad"
# RABBIT MQ
RMQ_URI=amqp://v3-broker:5672
RMQ_EXCHANGE=mobicoop
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# REDIS
REDIS_HOST=v3-redis

115
README.md
View File

@ -58,22 +58,18 @@ The app exposes the following [gRPC](https://grpc.io/) services :
- **Create** : create an ad (note that uuid is optional, a uuid will be automatically attributed if it is not provided)
Punctual driver ad :
```json
{
"userUuid": "113e0000-0000-4000-a000-000000000000",
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
"driver": true,
"passenger": false,
"frequency": "PUNCTUAL",
"departure": "2023-01-15",
"toDate": "2023-08-01",
"marginDurations": {
"mon": 800
},
"seatsPassenger": 0,
"seatsDriver": 3,
"frequency": "PUNCTUAL",
"departure": "2023-01-15 09:00",
"addresses": [
{
"position": 0,
"lon": 48.68944505415954,
"lat": 6.176510296462267,
"houseNumber": "5",
@ -83,6 +79,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"country": "France"
},
{
"position": 1,
"lon": 48.8566,
"lat": 2.3522,
"locality": "Paris",
@ -93,6 +90,104 @@ The app exposes the following [gRPC](https://grpc.io/) services :
}
```
Punctual driver and passenger ad :
```json
{
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
"driver": true,
"pasenger": true,
"seatsDriver": 3,
"seatsPassenger": 1,
"frequency": "PUNCTUAL",
"departure": "2023-01-15 09:00",
"addresses": [
{
"position": 0,
"lon": 48.68944505415954,
"lat": 6.176510296462267,
"houseNumber": "5",
"street": "Avenue Foch",
"locality": "Nancy",
"postalCode": "54000",
"country": "France"
},
{
"position": 1,
"lon": 48.8566,
"lat": 2.3522,
"locality": "Paris",
"postalCode": "75000",
"country": "France"
}
]
}
```
Recurrent passenger ad :
```json
{
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
"passenger": true,
"seatsPassenger": 1,
"frequency": "RECURRRENT",
"fromDate": "2023-01-15",
"toDate": "2023-12-31",
"schedule": {
"mon": "07:00",
"tue": "07:05",
"fri": "07:10"
},
"addresses": [
{
"position": 0,
"lon": 48.68944505415954,
"lat": 6.176510296462267,
"houseNumber": "5",
"street": "Avenue Foch",
"locality": "Nancy",
"postalCode": "54000",
"country": "France"
},
{
"position": 1,
"lon": 48.8566,
"lat": 2.3522,
"locality": "Paris",
"postalCode": "75000",
"country": "France"
}
]
}
```
The list of possible options when creating an ad :
- uuid (optional): the uuid of the ad
- userUuid: the user uuid
- driver (boolean, optional): if the ad is a driver ad
- passenger (boolean, optional): if the ad is a passenger ad
- frequency: `PUNCTUAL` or `RECURRENT`
- departure (required if punctual): departure date and hour/minute for a punctual ad
- fromDate (required if recurrent): start date for recurrent ad
- toDate (required if recurrent): end date for recurrent ad
- schedule (required if recurrent): an object with the departure time for each carpooled day in the week
- marginDurations (optional): an object with the margin duration (in seconds) for each carpooled day in the week, eg:
{
"mon": 900,
"tue": 850,
"fri": 950
}
- seatsDriver (optional): number of seats proposed as driver;
- seatsPassenger (optional): number of seats requested as passenger;
- strict (boolean, optional): if set to true, allow matching only with similar frequency ads
- addresses: an array of adresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads)
Default values must be set in `.env` file.
## Messages
As mentionned earlier, RabbitMQ messages are sent after these events :

148
package-lock.json generated
View File

@ -12,10 +12,11 @@
"@automapper/classes": "^8.7.7",
"@automapper/core": "^8.7.7",
"@automapper/nestjs": "^8.7.7",
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.0.5",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
@ -56,6 +57,24 @@
"typescript": "^4.7.4"
}
},
"node_modules/@acuminous/bitsyntax": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz",
"integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==",
"dependencies": {
"buffer-more-ints": "~1.0.0",
"debug": "^4.3.4",
"safe-buffer": "~5.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@ -1548,6 +1567,106 @@
"node": ">=8"
}
},
"node_modules/@mobicoop/configuration-module": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.1.0.tgz",
"integrity": "sha512-4yzCrY8m40XOO3CZnWJC4kHk66sTQCwe5UjKCV/UpNkN9IGUKW+R84J/53aulmGTL95vec7g6tFIwlHJd9BCoA==",
"dependencies": {
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/message-broker-module": "^1.0.4",
"@nestjs/cqrs": "^9.0.4",
"@types/amqplib": "^0.10.1",
"amqplib": "^0.10.3",
"class-validator": "^0.14.0",
"ioredis": "^5.3.2"
},
"peerDependencies": {
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/configuration-module/node_modules/amqplib": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz",
"integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==",
"dependencies": {
"@acuminous/bitsyntax": "^0.1.2",
"buffer-more-ints": "~1.0.0",
"readable-stream": "1.x >=1.1.9",
"url-parse": "~1.5.10"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@mobicoop/configuration-module/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/@mobicoop/configuration-module/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/@mobicoop/configuration-module/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/@mobicoop/message-broker-module": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.0.6.tgz",
"integrity": "sha512-aVkWErc5pHz1oPRVBzvK3CvKKcUSNDvW58fbFXHbOA+md+jnyP9sH9NHyIOtVzIv0f6DbJBn9SA3x4VnSrDaBg==",
"dependencies": {
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@types/amqplib": "^0.10.1",
"amqplib": "^0.10.3"
},
"peerDependencies": {
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/message-broker-module/node_modules/amqplib": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz",
"integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==",
"dependencies": {
"@acuminous/bitsyntax": "^0.1.2",
"buffer-more-ints": "~1.0.0",
"readable-stream": "1.x >=1.1.9",
"url-parse": "~1.5.10"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@mobicoop/message-broker-module/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/@mobicoop/message-broker-module/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/@mobicoop/message-broker-module/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/@nestjs/cli": {
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.4.2.tgz",
@ -1632,12 +1751,12 @@
}
},
"node_modules/@nestjs/common": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.4.0.tgz",
"integrity": "sha512-RUcVAQsEF4WPrmzFXEOUfZnPwrLTe1UVlzXTlSyfqfqbdWDPKDGlIPVelBLfc5/+RRUQ0I5iE4+CQvpCmkqldw==",
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.4.2.tgz",
"integrity": "sha512-sea+qZnbD5x3YWZDVQT/wbVJ2NiABaM1tyZTLuW9hpkcM2KFA96xKtK3VaCxyz49zoXIgSOefsyK7HuUMCe27Q==",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.5.0",
"tslib": "2.5.2",
"uid": "2.0.2"
},
"funding": {
@ -1663,6 +1782,11 @@
}
}
},
"node_modules/@nestjs/common/node_modules/tslib": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA=="
},
"node_modules/@nestjs/config": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-2.3.1.tgz",
@ -1717,9 +1841,9 @@
}
},
"node_modules/@nestjs/cqrs": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/cqrs/-/cqrs-9.0.3.tgz",
"integrity": "sha512-hmbrqf51BVdgmnnxErnLVXfPNTEqr4Hz8DyLa9dKLIW3BuOyI5RDwJ/9sKbJ47UDBhumC5nQlNK9qk27mhqHfw==",
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@nestjs/cqrs/-/cqrs-9.0.4.tgz",
"integrity": "sha512-nWDF+xs4jqs6OjxFg/wVSd0NiIV9+EFCJrJNTo4VRWe78CcAaitbp56CBspUh4gKyfkci95i+EhHdEqRXKFptg==",
"dependencies": {
"uuid": "9.0.0"
},
@ -2162,6 +2286,14 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/amqplib": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.1.tgz",
"integrity": "sha512-j6ANKT79ncUDnAs/+9r9eDujxbeJoTjoVu33gHHcaPfmLQaMhvfbH2GqSe8KUM444epAp1Vl3peVOQfZk3UIqA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz",

View File

@ -37,10 +37,11 @@
"@automapper/classes": "^8.7.7",
"@automapper/core": "^8.7.7",
"@automapper/nestjs": "^8.7.7",
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.0.5",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
@ -93,6 +94,7 @@
".presenter.ts",
".profile.ts",
".exception.ts",
".constants.ts",
"main.ts"
],
"rootDir": "src",
@ -111,6 +113,8 @@
".presenter.ts",
".profile.ts",
".exception.ts",
".constants.ts",
".interfaces.ts",
"main.ts"
],
"coverageDirectory": "../coverage",

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

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

View File

@ -1,16 +1,65 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigurationModule } from './modules/configuration/configuration.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HealthModule } from './modules/health/health.module';
import { AdModule } from './modules/ad/ad.module';
import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
import {
ConfigurationModule,
ConfigurationModuleOptions,
} from '@mobicoop/configuration-module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
ConfigurationModule,
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'),
}),
},
false,
),
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'),
},
setConfigurationBrokerRoutingKeys: [
'configuration.create',
'configuration.update',
],
deleteConfigurationRoutingKey: 'configuration.delete',
propagateConfigurationRoutingKey: 'configuration.propagate',
setConfigurationBrokerQueue: 'ad-configuration-create-update',
deleteConfigurationQueue: 'ad-configuration-delete',
propagateConfigurationQueue: 'ad-configuration-propagate',
}),
},
true,
),
HealthModule,
AdModule,
],

View File

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

View File

@ -0,0 +1 @@
export const PARAMS_PROVIDER = Symbol();

View File

@ -2,45 +2,36 @@ import { Module } from '@nestjs/common';
import { AdController } from './adapters/primaries/ad.controller';
import { DatabaseModule } from '../database/database.module';
import { CqrsModule } from '@nestjs/cqrs';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AdProfile } from './mappers/ad.profile';
import { AdsRepository } from './adapters/secondaries/ads.repository';
import { Messager } from './adapters/secondaries/messager';
import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase';
import { CreateAdUseCase } from './domain/usecases/create-ad.usecase';
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
import { PARAMS_PROVIDER } from './ad.constants';
import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { MessagePublisher } from './adapters/secondaries/message-publisher';
@Module({
imports: [
DatabaseModule,
CqrsModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
],
imports: [DatabaseModule, CqrsModule],
controllers: [AdController],
providers: [
AdProfile,
AdsRepository,
Messager,
FindAdByUuidUseCase,
CreateAdUseCase,
{
provide: 'ParamsProvider',
provide: PARAMS_PROVIDER,
useClass: DefaultParamsProvider,
},
{
provide: MESSAGE_BROKER_PUBLISHER,
useClass: MessageBrokerPublisher,
},
{
provide: MESSAGE_PUBLISHER,
useClass: MessagePublisher,
},
],
})
export class AdModule {}

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

@ -1,18 +0,0 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class Messager extends IMessageBroker {
constructor(
private readonly amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish(routingKey: string, message: string): void {
this.amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@ -1,12 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IMessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

View File

@ -2,7 +2,6 @@ import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { Inject } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager';
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
import { CreateAdCommand } from '../../commands/create-ad.command';
import { CreateAdRequest } from '../dtos/create-ad.request';
@ -10,6 +9,9 @@ import { IProvideParams } from '../interfaces/param-provider.interface';
import { DefaultParams } from '../types/default-params.type';
import { AdCreation } from '../dtos/ad.creation';
import { Ad } from '../entities/ad';
import { PARAMS_PROVIDER } from '../../ad.constants';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
@CommandHandler(CreateAdCommand)
export class CreateAdUseCase {
@ -17,9 +19,10 @@ export class CreateAdUseCase {
private ad: AdCreation;
constructor(
private readonly repository: AdsRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
@InjectMapper() private readonly mapper: Mapper,
@Inject('ParamsProvider')
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: IProvideParams,
) {
this.defaultParams = defaultParamsProvider.getParams();
@ -38,8 +41,8 @@ export class CreateAdUseCase {
try {
const adCreated: Ad = await this.repository.create(this.ad);
this.messager.publish('ad.create', JSON.stringify(adCreated));
this.messager.publish(
this.messagePublisher.publish('ad.create', JSON.stringify(adCreated));
this.messagePublisher.publish(
'logging.ad.create.info',
JSON.stringify(adCreated),
);
@ -49,7 +52,7 @@ export class CreateAdUseCase {
if (error.message.includes('Already exists')) {
key = 'logging.ad.create.warning';
}
this.messager.publish(
this.messagePublisher.publish(
key,
JSON.stringify({
command,

View File

@ -1,15 +1,17 @@
import { NotFoundException } from '@nestjs/common';
import { Inject, NotFoundException } from '@nestjs/common';
import { QueryHandler } from '@nestjs/cqrs';
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
import { Messager } from '../../adapters/secondaries/messager';
import { Ad } from '../entities/ad';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@QueryHandler(FindAdByUuidQuery)
export class FindAdByUuidUseCase {
constructor(
private readonly repository: AdsRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
) {}
async execute(findAdByUuid: FindAdByUuidQuery): Promise<Ad> {
@ -18,7 +20,7 @@ export class FindAdByUuidUseCase {
if (!ad) throw new NotFoundException();
return ad;
} catch (error) {
this.messager.publish(
this.messagePublisher.publish(
'logging.ad.read.warning',
JSON.stringify({
query: findAdByUuid,

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,47 +0,0 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../../../adapters/secondaries/messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
const mockConfigService = {
get: jest.fn().mockResolvedValue({
RMQ_EXCHANGE: 'mobicoop',
}),
};
describe('Messager', () => {
let messager: Messager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
Messager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
messager = module.get<Messager>(Messager);
});
it('should be defined', () => {
expect(messager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
messager.publish('ad.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,7 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase';
import { CreateAdRequest } from '../../../domain/dtos/create-ad.request';
import { Messager } from '../../../adapters/secondaries/messager';
import { AdsRepository } from '../../../adapters/secondaries/ads.repository';
import { CreateAdCommand } from '../../../commands/create-ad.command';
import { AutomapperModule } from '@automapper/nestjs';
@ -11,7 +10,9 @@ import { Ad } from '../../../domain/entities/ad';
import { AdProfile } from '../../../mappers/ad.profile';
import { AddressDTO } from '../../../domain/dtos/address.dto';
import { AdCreation } from '../../../domain/dtos/ad.creation';
import { Address } from 'src/modules/ad/domain/entities/address';
import { Address } from '../../../domain/entities/address';
import { PARAMS_PROVIDER } from '../../../ad.constants';
import { MESSAGE_PUBLISHER } from '../../../../../app.constants';
const mockAddress1: AddressDTO = {
position: 0,
@ -80,7 +81,7 @@ const newAdRequest: CreateAdRequest = {
addresses: [mockAddress1, mockAddress2],
};
const mockMessager = {
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
const mockDefaultParamsProvider = {
@ -128,13 +129,13 @@ describe('CreateAdUseCase', () => {
useValue: mockAdRepository,
},
{
provide: Messager,
useValue: mockMessager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
CreateAdUseCase,
AdProfile,
{
provide: 'ParamsProvider',
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
],

View File

@ -1,10 +1,10 @@
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../../adapters/secondaries/messager';
import { FindAdByUuidQuery } from '../../../queries/find-ad-by-uuid.query';
import { AdsRepository } from '../../../adapters/secondaries/ads.repository';
import { FindAdByUuidUseCase } from '../../../domain/usecases/find-ad-by-uuid.usecase';
import { FindAdByUuidRequest } from '../../../domain/dtos/find-ad-by-uuid.request';
import { MESSAGE_PUBLISHER } from '../../../../../app.constants';
const mockAd = {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
@ -22,7 +22,7 @@ const mockAdRepository = {
}),
};
const mockMessager = {
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
@ -37,8 +37,8 @@ describe('FindAdByUuidUseCase', () => {
useValue: mockAdRepository,
},
{
provide: Messager,
useValue: mockMessager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
FindAdByUuidUseCase,
],

View File

@ -1,77 +0,0 @@
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
import { Controller } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CommandBus } from '@nestjs/cqrs';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
import { SetConfigurationCommand } from '../../commands/set-configuration.command';
import { DeleteConfigurationRequest } from '../../domain/dtos/delete-configuration.request';
import { SetConfigurationRequest } from '../../domain/dtos/set-configuration.request';
import { Configuration } from '../../domain/entities/configuration';
@Controller()
export class ConfigurationMessagerController {
constructor(
private readonly _commandBus: CommandBus,
private readonly _configService: ConfigService,
) {}
@RabbitSubscribe({
name: 'setConfiguration',
})
public async setConfigurationHandler(message: string) {
const configuration: Configuration = JSON.parse(message);
if (
configuration.domain ==
this._configService.get<string>('SERVICE_CONFIGURATION_DOMAIN')
) {
const setConfigurationRequest: SetConfigurationRequest =
new SetConfigurationRequest();
setConfigurationRequest.domain = configuration.domain;
setConfigurationRequest.key = configuration.key;
setConfigurationRequest.value = configuration.value;
await this._commandBus.execute(
new SetConfigurationCommand(setConfigurationRequest),
);
}
}
@RabbitSubscribe({
name: 'deleteConfiguration',
})
public async configurationDeletedHandler(message: string) {
const deletedConfiguration: Configuration = JSON.parse(message);
if (
deletedConfiguration.domain ==
this._configService.get<string>('SERVICE_CONFIGURATION_DOMAIN')
) {
const deleteConfigurationRequest = new DeleteConfigurationRequest();
deleteConfigurationRequest.domain = deletedConfiguration.domain;
deleteConfigurationRequest.key = deletedConfiguration.key;
await this._commandBus.execute(
new DeleteConfigurationCommand(deleteConfigurationRequest),
);
}
}
@RabbitSubscribe({
name: 'propagateConfiguration',
})
public async propagateConfigurationsHandler(message: string) {
const configurations: Array<Configuration> = JSON.parse(message);
configurations.forEach(async (configuration) => {
if (
configuration.domain ==
this._configService.get<string>('SERVICE_CONFIGURATION_DOMAIN')
) {
const setConfigurationRequest: SetConfigurationRequest =
new SetConfigurationRequest();
setConfigurationRequest.domain = configuration.domain;
setConfigurationRequest.key = configuration.key;
setConfigurationRequest.value = configuration.value;
await this._commandBus.execute(
new SetConfigurationCommand(setConfigurationRequest),
);
}
});
}
}

View File

@ -1,23 +0,0 @@
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';
import { IConfigurationRepository } from '../../domain/interfaces/configuration.repository';
@Injectable()
export class RedisConfigurationRepository extends IConfigurationRepository {
constructor(@InjectRedis() private readonly _redis: Redis) {
super();
}
async get(key: string): Promise<string> {
return await this._redis.get(key);
}
async set(key: string, value: string) {
await this._redis.set(key, value);
}
async del(key: string) {
await this._redis.del(key);
}
}

View File

@ -1,9 +0,0 @@
import { DeleteConfigurationRequest } from '../domain/dtos/delete-configuration.request';
export class DeleteConfigurationCommand {
readonly deleteConfigurationRequest: DeleteConfigurationRequest;
constructor(deleteConfigurationRequest: DeleteConfigurationRequest) {
this.deleteConfigurationRequest = deleteConfigurationRequest;
}
}

View File

@ -1,9 +0,0 @@
import { SetConfigurationRequest } from '../domain/dtos/set-configuration.request';
export class SetConfigurationCommand {
readonly setConfigurationRequest: SetConfigurationRequest;
constructor(setConfigurationRequest: SetConfigurationRequest) {
this.setConfigurationRequest = setConfigurationRequest;
}
}

View File

@ -1,68 +0,0 @@
import { RabbitMQConfig, RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { ConfigurationMessagerController } from './adapters/primaries/configuration-messager.controller';
import { RedisConfigurationRepository } from './adapters/secondaries/redis-configuration.repository';
import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase';
import { GetConfigurationUseCase } from './domain/usecases/get-configuration.usecase';
import { SetConfigurationUseCase } from './domain/usecases/set-configuration.usecase';
@Module({
imports: [
CqrsModule,
RedisModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<RedisModuleOptions> => ({
config: {
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
password: configService.get<string>('REDIS_PASSWORD'),
},
}),
}),
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<RabbitMQConfig> => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
handlers: {
setConfiguration: {
exchange: configService.get<string>('RMQ_EXCHANGE'),
routingKey: ['configuration.create', 'configuration.update'],
},
deleteConfiguration: {
exchange: configService.get<string>('RMQ_EXCHANGE'),
routingKey: 'configuration.delete',
},
propagateConfiguration: {
exchange: configService.get<string>('RMQ_EXCHANGE'),
routingKey: 'configuration.propagate',
},
},
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
enableControllerDiscovery: true,
}),
}),
],
controllers: [ConfigurationMessagerController],
providers: [
GetConfigurationUseCase,
SetConfigurationUseCase,
DeleteConfigurationUseCase,
RedisConfigurationRepository,
],
})
export class ConfigurationModule {}

View File

@ -1,11 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteConfigurationRequest {
@IsString()
@IsNotEmpty()
domain: string;
@IsString()
@IsNotEmpty()
key: string;
}

View File

@ -1,15 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class SetConfigurationRequest {
@IsString()
@IsNotEmpty()
domain: string;
@IsString()
@IsNotEmpty()
key: string;
@IsString()
@IsNotEmpty()
value: string;
}

View File

@ -1,12 +0,0 @@
import { AutoMap } from '@automapper/classes';
export class Configuration {
@AutoMap()
domain: string;
@AutoMap()
key: string;
@AutoMap()
value: string;
}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IConfigurationRepository {
abstract get(key: string): Promise<string>;
abstract set(key: string, value: string): void;
abstract del(key: string): void;
}

View File

@ -1,16 +0,0 @@
import { CommandHandler } from '@nestjs/cqrs';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
@CommandHandler(DeleteConfigurationCommand)
export class DeleteConfigurationUseCase {
constructor(private _configurationRepository: RedisConfigurationRepository) {}
async execute(deleteConfigurationCommand: DeleteConfigurationCommand) {
await this._configurationRepository.del(
deleteConfigurationCommand.deleteConfigurationRequest.domain +
':' +
deleteConfigurationCommand.deleteConfigurationRequest.key,
);
}
}

View File

@ -1,14 +0,0 @@
import { QueryHandler } from '@nestjs/cqrs';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { GetConfigurationQuery } from '../../queries/get-configuration.query';
@QueryHandler(GetConfigurationQuery)
export class GetConfigurationUseCase {
constructor(private _configurationRepository: RedisConfigurationRepository) {}
async execute(getConfigurationQuery: GetConfigurationQuery): Promise<string> {
return this._configurationRepository.get(
getConfigurationQuery.domain + ':' + getConfigurationQuery.key,
);
}
}

View File

@ -1,17 +0,0 @@
import { CommandHandler } from '@nestjs/cqrs';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { SetConfigurationCommand } from '../../commands/set-configuration.command';
@CommandHandler(SetConfigurationCommand)
export class SetConfigurationUseCase {
constructor(private _configurationRepository: RedisConfigurationRepository) {}
async execute(setConfigurationCommand: SetConfigurationCommand) {
await this._configurationRepository.set(
setConfigurationCommand.setConfigurationRequest.domain +
':' +
setConfigurationCommand.setConfigurationRequest.key,
setConfigurationCommand.setConfigurationRequest.value,
);
}
}

View File

@ -1,9 +0,0 @@
export class GetConfigurationQuery {
readonly domain: string;
readonly key: string;
constructor(domain: string, key: string) {
this.domain = domain;
this.key = key;
}
}

View File

@ -1,49 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
import { DeleteConfigurationRequest } from '../../domain/dtos/delete-configuration.request';
import { DeleteConfigurationUseCase } from '../../domain/usecases/delete-configuration.usecase';
const mockRedisConfigurationRepository = {
del: jest.fn().mockResolvedValue(undefined),
};
describe('DeleteConfigurationUseCase', () => {
let deleteConfigurationUseCase: DeleteConfigurationUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: RedisConfigurationRepository,
useValue: mockRedisConfigurationRepository,
},
DeleteConfigurationUseCase,
],
}).compile();
deleteConfigurationUseCase = module.get<DeleteConfigurationUseCase>(
DeleteConfigurationUseCase,
);
});
it('should be defined', () => {
expect(deleteConfigurationUseCase).toBeDefined();
});
describe('execute', () => {
it('should delete a key', async () => {
jest.spyOn(mockRedisConfigurationRepository, 'del');
const deleteConfigurationRequest: DeleteConfigurationRequest = {
domain: 'my-domain',
key: 'my-key',
};
await deleteConfigurationUseCase.execute(
new DeleteConfigurationCommand(deleteConfigurationRequest),
);
expect(mockRedisConfigurationRepository.del).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -1,43 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { GetConfigurationUseCase } from '../../domain/usecases/get-configuration.usecase';
import { GetConfigurationQuery } from '../../queries/get-configuration.query';
const mockRedisConfigurationRepository = {
get: jest.fn().mockResolvedValue('my-value'),
};
describe('GetConfigurationUseCase', () => {
let getConfigurationUseCase: GetConfigurationUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: RedisConfigurationRepository,
useValue: mockRedisConfigurationRepository,
},
GetConfigurationUseCase,
],
}).compile();
getConfigurationUseCase = module.get<GetConfigurationUseCase>(
GetConfigurationUseCase,
);
});
it('should be defined', () => {
expect(getConfigurationUseCase).toBeDefined();
});
describe('execute', () => {
it('should get a value for a key', async () => {
const value: string = await getConfigurationUseCase.execute(
new GetConfigurationQuery('my-domain', 'my-key'),
);
expect(value).toBe('my-value');
});
});
});

View File

@ -1,47 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { getRedisToken } from '@liaoliaots/nestjs-redis';
const mockRedis = {
get: jest.fn().mockResolvedValue('myValue'),
set: jest.fn().mockImplementation(),
del: jest.fn().mockImplementation(),
};
describe('RedisConfigurationRepository', () => {
let redisConfigurationRepository: RedisConfigurationRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getRedisToken('default'),
useValue: mockRedis,
},
RedisConfigurationRepository,
],
}).compile();
redisConfigurationRepository = module.get<RedisConfigurationRepository>(
RedisConfigurationRepository,
);
});
it('should be defined', () => {
expect(redisConfigurationRepository).toBeDefined();
});
describe('interact', () => {
it('should get a value', async () => {
expect(await redisConfigurationRepository.get('myKey')).toBe('myValue');
});
it('should set a value', async () => {
expect(
await redisConfigurationRepository.set('myKey', 'myValue'),
).toBeUndefined();
});
it('should delete a value', async () => {
expect(await redisConfigurationRepository.del('myKey')).toBeUndefined();
});
});
});

View File

@ -1,50 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { SetConfigurationCommand } from '../../commands/set-configuration.command';
import { SetConfigurationRequest } from '../../domain/dtos/set-configuration.request';
import { SetConfigurationUseCase } from '../../domain/usecases/set-configuration.usecase';
const mockRedisConfigurationRepository = {
set: jest.fn().mockResolvedValue(undefined),
};
describe('SetConfigurationUseCase', () => {
let setConfigurationUseCase: SetConfigurationUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: RedisConfigurationRepository,
useValue: mockRedisConfigurationRepository,
},
SetConfigurationUseCase,
],
}).compile();
setConfigurationUseCase = module.get<SetConfigurationUseCase>(
SetConfigurationUseCase,
);
});
it('should be defined', () => {
expect(setConfigurationUseCase).toBeDefined();
});
describe('execute', () => {
it('should set a value for a key', async () => {
jest.spyOn(mockRedisConfigurationRepository, 'set');
const setConfigurationRequest: SetConfigurationRequest = {
domain: 'my-domain',
key: 'my-key',
value: 'my-value',
};
await setConfigurationUseCase.execute(
new SetConfigurationCommand(setConfigurationRequest),
);
expect(mockRedisConfigurationRepository.set).toHaveBeenCalledTimes(1);
});
});
});

View File

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

View File

@ -1,30 +1,33 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Inject } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
import { Messager } from '../secondaries/messager';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
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 _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
private _healthCheckService: HealthCheckService,
private _messager: Messager,
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._prismaHealthIndicatorUseCase.isHealthy('prisma'),
return await this.healthCheckService.check([
async () =>
this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
]);
} catch (error) {
const healthCheckResult: HealthCheckResult = error.response;
this._messager.publish(
this.messagePublisher.publish(
'logging.user.health.crit',
JSON.stringify(healthCheckResult.error),
);

View File

@ -1,12 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IMessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

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

@ -1,18 +0,0 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IMessageBroker } from './message-broker';
@Injectable()
export class Messager extends IMessageBroker {
constructor(
private readonly _amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

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

View File

@ -1,25 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { AdsRepository } from '../../../ad/adapters/secondaries/ads.repository';
@Injectable()
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
constructor(private readonly _repository: AdsRepository) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await this._repository.healthCheck();
return this.getStatus(key, true);
} catch (e) {
throw new HealthCheckError('Prisma', {
prisma: e.message,
});
}
}
}

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 { AdsRepository } from '../../../ad/adapters/secondaries/ads.repository';
@Injectable()
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
private checkRepositories: ICheckRepository[];
constructor(private readonly adsRepository: AdsRepository) {
super();
this.checkRepositories = [adsRepository];
}
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

@ -1,34 +1,28 @@
import { Module } from '@nestjs/common';
import { HealthServerController } from './adapters/primaries/health-server.controller';
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
import { AdsRepository } from '../ad/adapters/secondaries/ads.repository';
import { DatabaseModule } from '../database/database.module';
import { HealthController } from './adapters/primaries/health.controller';
import { TerminusModule } from '@nestjs/terminus';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Messager } from './adapters/secondaries/messager';
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';
@Module({
imports: [
TerminusModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
DatabaseModule,
],
imports: [TerminusModule, DatabaseModule],
controllers: [HealthServerController, HealthController],
providers: [PrismaHealthIndicatorUseCase, AdsRepository, Messager],
providers: [
RepositoriesHealthIndicatorUseCase,
AdsRepository,
{
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,47 +0,0 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
const mockConfigService = {
get: jest.fn().mockResolvedValue({
RMQ_EXCHANGE: 'mobicoop',
}),
};
describe('Messager', () => {
let messager: Messager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
Messager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
messager = module.get<Messager>(Messager);
});
it('should be defined', () => {
expect(messager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
messager.publish('test.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

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