mirror of
https://gitlab.com/mobicoop/v3/service/ad.git
synced 2026-01-03 08:22:41 +00:00
upgrade health, use configuration and broker packages
This commit is contained in:
2
src/app.constants.ts
Normal file
2
src/app.constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MESSAGE_BROKER_PUBLISHER = Symbol();
|
||||
export const MESSAGE_PUBLISHER = Symbol();
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
3
src/interfaces/message-publisher.ts
Normal file
3
src/interfaces/message-publisher.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IPublishMessage {
|
||||
publish(routingKey: string, message: string): void;
|
||||
}
|
||||
1
src/modules/ad/ad.constants.ts
Normal file
1
src/modules/ad/ad.constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PARAMS_PROVIDER = Symbol();
|
||||
@@ -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 {}
|
||||
|
||||
16
src/modules/ad/adapters/secondaries/message-publisher.ts
Normal file
16
src/modules/ad/adapters/secondaries/message-publisher.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { DeleteConfigurationRequest } from '../domain/dtos/delete-configuration.request';
|
||||
|
||||
export class DeleteConfigurationCommand {
|
||||
readonly deleteConfigurationRequest: DeleteConfigurationRequest;
|
||||
|
||||
constructor(deleteConfigurationRequest: DeleteConfigurationRequest) {
|
||||
this.deleteConfigurationRequest = deleteConfigurationRequest;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { SetConfigurationRequest } from '../domain/dtos/set-configuration.request';
|
||||
|
||||
export class SetConfigurationCommand {
|
||||
readonly setConfigurationRequest: SetConfigurationRequest;
|
||||
|
||||
constructor(setConfigurationRequest: SetConfigurationRequest) {
|
||||
this.setConfigurationRequest = setConfigurationRequest;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteConfigurationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
domain: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
key: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class Configuration {
|
||||
@AutoMap()
|
||||
domain: string;
|
||||
|
||||
@AutoMap()
|
||||
key: string;
|
||||
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class GetConfigurationQuery {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
|
||||
constructor(domain: string, key: string) {
|
||||
this.domain = domain;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
16
src/modules/health/adapters/secondaries/message-publisher.ts
Normal file
16
src/modules/health/adapters/secondaries/message-publisher.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ICheckRepository {
|
||||
healthCheck(): Promise<boolean>;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
36
src/modules/health/tests/unit/message-publisher.spec.ts
Normal file
36
src/modules/health/tests/unit/message-publisher.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user