remove configuration and rabbit modules, add corresponding packages

This commit is contained in:
sbriat 2023-06-02 15:56:23 +02:00
parent b4fdadda37
commit e72ecefd0a
45 changed files with 2561 additions and 9156 deletions

10733
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,10 +34,12 @@
"@automapper/classes": "^8.7.7",
"@automapper/core": "^8.7.7",
"@automapper/nestjs": "^8.7.7",
"@golevelup/nestjs-rabbitmq": "^3.4.0",
"@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.4",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.0.5",
"@nestjs/cache-manager": "^1.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
@ -49,7 +51,7 @@
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"cache-manager": "^5.1.4",
"cache-manager": "^5.2.1",
"cache-manager-ioredis-yet": "^1.1.0",
"class-transformer": "^0.5.1",
"dotenv-cli": "^6.0.0",

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,17 +1,67 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
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 { UserModule } from './modules/user/user.module';
import {
ConfigurationModule,
ConfigurationModuleOptions,
} from '@mobicoop/configuration-module';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
UserModule,
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'),
handlers: {},
}),
},
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: 'user-configuration-create-update',
deleteConfigurationQueue: 'user-configuration-delete',
propagateConfigurationQueue: 'user-configuration-propagate',
}),
},
true,
),
HealthModule,
],
controllers: [],

View File

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

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,18 +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();
}
get = async (key: string): Promise<string> => await this.redis.get(key);
set = async (key: string, value: string): Promise<'OK'> =>
this.redis.set(key, value);
del = async (key: string): Promise<number> => 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,18 +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) {}
execute = async (
deleteConfigurationCommand: DeleteConfigurationCommand,
): Promise<void> => {
await this.configurationRepository.del(
deleteConfigurationCommand.deleteConfigurationRequest.domain +
':' +
deleteConfigurationCommand.deleteConfigurationRequest.key,
);
};
}

View File

@ -1,15 +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) {}
execute = async (
getConfigurationQuery: GetConfigurationQuery,
): Promise<string> =>
this.configurationRepository.get(
getConfigurationQuery.domain + ':' + getConfigurationQuery.key,
);
}

View File

@ -1,19 +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) {}
execute = async (
setConfigurationCommand: SetConfigurationCommand,
): Promise<void> => {
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,18 +1,20 @@
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';
@Controller('health')
export class HealthController {
constructor(
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
private readonly healthCheckService: HealthCheckService,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
) {}
@Get()
@ -24,7 +26,7 @@ export class HealthController {
]);
} 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,
private readonly configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish = (routingKey: string, message: string): void => {
this.amqpConnection.publish(this.exchange, routingKey, message);
};
}

View File

@ -5,30 +5,24 @@ import { UsersRepository } from '../user/adapters/secondaries/users.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';
@Module({
imports: [
TerminusModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
imports: [TerminusModule, DatabaseModule],
controllers: [HealthServerController, HealthController],
providers: [
PrismaHealthIndicatorUseCase,
UsersRepository,
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
provide: MESSAGE_BROKER_PUBLISHER,
useClass: MessageBrokerPublisher,
},
{
provide: MESSAGE_PUBLISHER,
useClass: MessagePublisher,
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
DatabaseModule,
],
controllers: [HealthServerController, HealthController],
providers: [PrismaHealthIndicatorUseCase, UsersRepository, Messager],
})
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,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
import { UsersRepository } from '../../../user/adapters/secondaries/users.repository';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
import { Prisma } from '@prisma/client';
const mockUsersRepository = {
healthCheck: jest
@ -11,7 +11,7 @@ const mockUsersRepository = {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new PrismaClientKnownRequestError('Service unavailable', {
throw new Prisma.PrismaClientKnownRequestError('Service unavailable', {
code: 'code',
clientVersion: 'version',
});

View File

@ -1,12 +1,6 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import {
CacheInterceptor,
CacheKey,
Controller,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Controller, UseInterceptors, UsePipes } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { DatabaseException } from '../../../database/exceptions/database.exception';
@ -23,6 +17,7 @@ import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
import { UserPresenter } from './user.presenter';
import { ICollection } from '../../../database/interfaces/collection.interface';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
@UsePipes(
new RpcValidationPipe({

View File

@ -0,0 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
@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,
private readonly 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

@ -1,17 +1,20 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { CommandHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { CreateUserCommand } from '../../commands/create-user.command';
import { CreateUserRequest } from '../dtos/create-user.request';
import { User } from '../entities/user';
import { Inject } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@CommandHandler(CreateUserCommand)
export class CreateUserUseCase {
constructor(
private readonly repository: UsersRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
@InjectMapper() private readonly mapper: Mapper,
) {}
@ -24,15 +27,18 @@ export class CreateUserUseCase {
try {
const user = await this.repository.create(entity);
this.messager.publish('user.create', JSON.stringify(user));
this.messager.publish('logging.user.create.info', JSON.stringify(user));
this.messagePublisher.publish('user.create', JSON.stringify(user));
this.messagePublisher.publish(
'logging.user.create.info',
JSON.stringify(user),
);
return user;
} catch (error) {
let key = 'logging.user.create.crit';
if (error.message.includes('Already exists')) {
key = 'logging.user.create.warning';
}
this.messager.publish(
this.messagePublisher.publish(
key,
JSON.stringify({
command,

View File

@ -1,27 +1,33 @@
import { CommandHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { DeleteUserCommand } from '../../commands/delete-user.command';
import { User } from '../entities/user';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
import { Inject } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
@CommandHandler(DeleteUserCommand)
export class DeleteUserUseCase {
constructor(
private readonly repository: UsersRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
) {}
execute = async (command: DeleteUserCommand): Promise<User> => {
try {
const user = await this.repository.delete(command.uuid);
this.messager.publish('user.delete', JSON.stringify({ uuid: user.uuid }));
this.messager.publish(
this.messagePublisher.publish(
'user.delete',
JSON.stringify({ uuid: user.uuid }),
);
this.messagePublisher.publish(
'logging.user.delete.info',
JSON.stringify({ uuid: user.uuid }),
);
return user;
} catch (error) {
this.messager.publish(
this.messagePublisher.publish(
'logging.user.delete.crit',
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 { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
import { User } from '../entities/user';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@QueryHandler(FindUserByUuidQuery)
export class FindUserByUuidUseCase {
constructor(
private readonly repository: UsersRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
) {}
execute = async (findUserByUuid: FindUserByUuidQuery): Promise<User> => {
@ -18,7 +20,7 @@ export class FindUserByUuidUseCase {
if (!user) throw new NotFoundException();
return user;
} catch (error) {
this.messager.publish(
this.messagePublisher.publish(
'logging.user.read.warning',
JSON.stringify({
query: findUserByUuid,

View File

@ -1,17 +1,20 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { CommandHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { UpdateUserCommand } from '../../commands/update-user.command';
import { UpdateUserRequest } from '../dtos/update-user.request';
import { User } from '../entities/user';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
import { Inject } from '@nestjs/common';
import { IPublishMessage } from '../../../../interfaces/message-publisher';
@CommandHandler(UpdateUserCommand)
export class UpdateUserUseCase {
constructor(
private readonly repository: UsersRepository,
private readonly messager: Messager,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: IPublishMessage,
@InjectMapper() private readonly mapper: Mapper,
) {}
@ -27,17 +30,17 @@ export class UpdateUserUseCase {
command.updateUserRequest.uuid,
entity,
);
this.messager.publish(
this.messagePublisher.publish(
'user.update',
JSON.stringify(command.updateUserRequest),
);
this.messager.publish(
this.messagePublisher.publish(
'logging.user.update.info',
JSON.stringify(command.updateUserRequest),
);
return user;
} catch (error) {
this.messager.publish(
this.messagePublisher.publish(
'logging.user.update.crit',
JSON.stringify({
command,

View File

@ -1,13 +1,13 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { CreateUserCommand } from '../../commands/create-user.command';
import { CreateUserRequest } from '../../domain/dtos/create-user.request';
import { User } from '../../domain/entities/user';
import { CreateUserUseCase } from '../../domain/usecases/create-user.usecase';
import { UserProfile } from '../../mappers/user.profile';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
const newUserRequest: CreateUserRequest = {
firstName: 'John',
@ -31,7 +31,7 @@ const mockUsersRepository = {
}),
};
const mockMessager = {
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
@ -49,8 +49,8 @@ describe('CreateUserUseCase', () => {
CreateUserUseCase,
UserProfile,
{
provide: Messager,
useValue: mockMessager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],
}).compile();

View File

@ -1,8 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { DeleteUserCommand } from '../../commands/delete-user.command';
import { DeleteUserUseCase } from '../../domain/usecases/delete-user.usecase';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
const mockUsers = [
{
@ -46,7 +46,7 @@ const mockUsersRepository = {
}),
};
const mockMessager = {
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
@ -62,8 +62,8 @@ describe('DeleteUserUseCase', () => {
},
DeleteUserUseCase,
{
provide: Messager,
useValue: mockMessager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],
}).compile();

View File

@ -1,10 +1,10 @@
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { FindUserByUuidRequest } from '../../domain/dtos/find-user-by-uuid.request';
import { FindUserByUuidUseCase } from '../../domain/usecases/find-user-by-uuid.usecase';
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
const mockUser = {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
@ -26,7 +26,7 @@ const mockUserRepository = {
}),
};
const mockMessager = {
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
@ -42,8 +42,8 @@ describe('FindUserByUuidUseCase', () => {
useValue: mockUserRepository,
},
{
provide: Messager,
useValue: mockMessager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
FindUserByUuidUseCase,
],

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('user.create.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('user.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,13 +1,13 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
import { UsersRepository } from '../../adapters/secondaries/users.repository';
import { UpdateUserCommand } from '../../commands/update-user.command';
import { UpdateUserRequest } from '../../domain/dtos/update-user.request';
import { User } from '../../domain/entities/user';
import { UpdateUserUseCase } from '../../domain/usecases/update-user.usecase';
import { UserProfile } from '../../mappers/user.profile';
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
const originalUser: User = new User();
originalUser.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
@ -54,7 +54,7 @@ describe('UpdateUserUseCase', () => {
UpdateUserUseCase,
UserProfile,
{
provide: Messager,
provide: MESSAGE_PUBLISHER,
useValue: mockMessager,
},
],

View File

@ -1,12 +1,10 @@
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { RedisClientOptions } from '@liaoliaots/nestjs-redis';
import { CacheModule, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { redisStore } from 'cache-manager-ioredis-yet';
import { DatabaseModule } from '../database/database.module';
import { UserController } from './adapters/primaries/user.controller';
import { Messager } from './adapters/secondaries/messager';
import { UsersRepository } from './adapters/secondaries/users.repository';
import { CreateUserUseCase } from './domain/usecases/create-user.usecase';
import { DeleteUserUseCase } from './domain/usecases/delete-user.usecase';
@ -14,25 +12,18 @@ import { FindAllUsersUseCase } from './domain/usecases/find-all-users.usecase';
import { FindUserByUuidUseCase } from './domain/usecases/find-user-by-uuid.usecase';
import { UpdateUserUseCase } from './domain/usecases/update-user.usecase';
import { UserProfile } from './mappers/user.profile';
import { CacheModule } from '@nestjs/cache-manager';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import {
MESSAGE_BROKER_PUBLISHER,
MESSAGE_PUBLISHER,
} from '../../app.constants';
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],
}),
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
@ -50,12 +41,19 @@ import { UserProfile } from './mappers/user.profile';
providers: [
UserProfile,
UsersRepository,
Messager,
FindAllUsersUseCase,
FindUserByUuidUseCase,
CreateUserUseCase,
UpdateUserUseCase,
DeleteUserUseCase,
{
provide: MESSAGE_BROKER_PUBLISHER,
useClass: MessageBrokerPublisher,
},
{
provide: MESSAGE_PUBLISHER,
useClass: MessagePublisher,
},
],
exports: [],
})