Merge branch 'refactor' into 'main'
Refactor to better hexagon See merge request v3/service/configuration!24
This commit is contained in:
commit
376260d903
|
@ -6,6 +6,7 @@ HEALTH_SERVICE_PORT=6003
|
|||
# PRISMA
|
||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration"
|
||||
|
||||
# RABBIT MQ
|
||||
RMQ_URI=amqp://v3-broker:5672
|
||||
RMQ_EXCHANGE=mobicoop
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
||||
|
|
40
README.md
40
README.md
|
@ -2,6 +2,8 @@
|
|||
|
||||
Configuration items management. Used to configure all services using a broker to disseminate the configuration items.
|
||||
|
||||
This service handles the persistence of the configuration items of all services in a database, and sends values _via_ the broker.
|
||||
|
||||
Each item consists in :
|
||||
|
||||
- a **uuid** : a unique identifier for the configuration item
|
||||
|
@ -15,7 +17,9 @@ Practically, it's the other way round as it's easier to use this configuration s
|
|||
|
||||
## Available domains
|
||||
|
||||
- **USER** : user related configuration item
|
||||
- **AD** : ad related configuration items
|
||||
- **MATCHER** : matching algotithm related configuration items
|
||||
- **USER** : user related configuration items
|
||||
|
||||
## Requirements
|
||||
|
||||
|
@ -63,24 +67,16 @@ npm run migrate
|
|||
|
||||
The app exposes the following [gRPC](https://grpc.io/) services :
|
||||
|
||||
- **FindByUuid** : find a configuration item by its uuid
|
||||
- **Get** : get a configuration item by its domain and key
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
"domain": "AD",
|
||||
"key": "seatsProposed"
|
||||
}
|
||||
```
|
||||
|
||||
- **FindAll** : find all configuration items; you can use pagination with `page` (default:_1_) and `perPage` (default:_10_)
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"perPage": 10
|
||||
}
|
||||
```
|
||||
|
||||
- **Create** : create a configuration item (note that uuid is optional, a uuid will be automatically attributed if it is not provided)
|
||||
- **Set** : create or update a configuration item
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -90,20 +86,12 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
}
|
||||
```
|
||||
|
||||
- **Update** : update a configuration item value
|
||||
- **Delete** : delete a configuration item by its domain and key
|
||||
|
||||
```json
|
||||
{
|
||||
"value": "value2",
|
||||
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae"
|
||||
}
|
||||
```
|
||||
|
||||
- **Delete** : delete a configuration item by its uuid
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
"domain": "AD",
|
||||
"key": "seatsProposed"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -117,9 +105,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
|
||||
As mentionned earlier, RabbitMQ messages are sent after these events :
|
||||
|
||||
- **Create** (message : the created configuration item informations)
|
||||
|
||||
- **Update** (message : the updated configuration item informations)
|
||||
- **Set** (message : the created / updated configuration item informations)
|
||||
|
||||
- **Delete** (message : the uuid of the deleted configuration item)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
108
package.json
108
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/configuration",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Mobicoop V3 Configuration Service",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
|
@ -31,50 +31,52 @@
|
|||
"migrate:deploy": "npx prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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.5",
|
||||
"@grpc/proto-loader": "^0.7.4",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/cqrs": "^9.0.1",
|
||||
"@nestjs/microservices": "^9.2.1",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
"@prisma/client": "^4.9.0",
|
||||
"@grpc/grpc-js": "^1.9.6",
|
||||
"@grpc/proto-loader": "^0.7.10",
|
||||
"@mobicoop/ddd-library": "^2.1.1",
|
||||
"@mobicoop/health-module": "^2.3.1",
|
||||
"@mobicoop/message-broker-module": "^2.1.1",
|
||||
"@nestjs/common": "^10.2.7",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.7",
|
||||
"@nestjs/cqrs": "^10.2.6",
|
||||
"@nestjs/event-emitter": "^2.0.2",
|
||||
"@nestjs/microservices": "^10.2.7",
|
||||
"@nestjs/platform-express": "^10.2.7",
|
||||
"@nestjs/terminus": "^10.1.1",
|
||||
"@prisma/client": "^5.4.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0"
|
||||
"rimraf": "^5.0.5",
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "28.1.8",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"dotenv-cli": "^7.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "28.1.3",
|
||||
"prettier": "^2.3.2",
|
||||
"prisma": "^4.9.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "28.0.8",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"typescript": "^4.7.4"
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.7",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/jest": "29.5.6",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/supertest": "^2.0.15",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"jest": "29.7.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "29.1.1",
|
||||
"ts-loader": "^9.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
@ -83,12 +85,13 @@
|
|||
"ts"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".constants.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
|
@ -100,15 +103,20 @@
|
|||
"**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".constants.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"moduleNameMapper": {
|
||||
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||
"^@src(.*)": "<rootDir>$1"
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// service
|
||||
export const SERVICE_NAME = 'configuration';
|
||||
|
||||
// grpc
|
||||
export const GRPC_PACKAGE_NAME = 'configuration';
|
||||
export const GRPC_SERVICE_NAME = 'ConfigurationService';
|
||||
|
||||
// messaging
|
||||
export const CONFIGURATION_SET_ROUTING_KEY = 'configuration.set';
|
||||
export const CONFIGURATION_DELETED_ROUTING_KEY = 'configuration.deleted';
|
||||
export const CONFIGURATION_PROPAGATED_ROUTING_KEY = 'configuration.propagated';
|
||||
|
||||
// health
|
||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||
export const HEALTH_CONFIGURATION_REPOSITORY = 'ConfigurationRepository';
|
||||
export const HEALTH_CRITICAL_LOGGING_KEY = 'logging.configuration.health.crit';
|
|
@ -1,18 +1,47 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import {
|
||||
HealthModule,
|
||||
HealthModuleOptions,
|
||||
HealthRepositoryPort,
|
||||
} from '@mobicoop/health-module';
|
||||
import { MessagerModule } from '@modules/messager/messager.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigurationModule } from './modules/configuration/configuration.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import {
|
||||
HEALTH_CONFIGURATION_REPOSITORY,
|
||||
HEALTH_CRITICAL_LOGGING_KEY,
|
||||
SERVICE_NAME,
|
||||
} from './app.constants';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationModule } from '@modules/configuration/configuration.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
||||
EventEmitterModule.forRoot(),
|
||||
HealthModule.forRootAsync({
|
||||
imports: [ConfigurationModule, MessagerModule],
|
||||
inject: [CONFIGURATION_REPOSITORY, MESSAGE_PUBLISHER],
|
||||
useFactory: async (
|
||||
configurationRepository: HealthRepositoryPort,
|
||||
messagePublisher: MessagePublisherPort,
|
||||
): Promise<HealthModuleOptions> => ({
|
||||
serviceName: SERVICE_NAME,
|
||||
criticalLoggingKey: HEALTH_CRITICAL_LOGGING_KEY,
|
||||
checkRepositories: [
|
||||
{
|
||||
name: HEALTH_CONFIGURATION_REPOSITORY,
|
||||
repository: configurationRepository,
|
||||
},
|
||||
],
|
||||
messagePublisher,
|
||||
}),
|
||||
}),
|
||||
ConfigurationModule,
|
||||
HealthModule,
|
||||
MessagerModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
exports: [ConfigurationModule, MessagerModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
|||
|
||||
package health;
|
||||
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
|||
NOT_SERVING = 2;
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
string message = 2;
|
||||
}
|
13
src/main.ts
13
src/main.ts
|
@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
|||
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import { GRPC_HEALTH_PACKAGE_NAME, GRPC_PACKAGE_NAME } from './app.constants';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
@ -11,20 +12,20 @@ async function bootstrap() {
|
|||
app.connectMicroservice<MicroserviceOptions>({
|
||||
transport: Transport.GRPC,
|
||||
options: {
|
||||
package: ['configuration', 'health'],
|
||||
package: [GRPC_PACKAGE_NAME, GRPC_HEALTH_PACKAGE_NAME],
|
||||
protoPath: [
|
||||
join(
|
||||
__dirname,
|
||||
'modules/configuration/adapters/primaries/configuration.proto',
|
||||
'modules/configuration/interface/grpc-controllers/configuration.proto',
|
||||
),
|
||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
||||
join(__dirname, 'health.proto'),
|
||||
],
|
||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||
loader: { keepCase: true },
|
||||
url: `${process.env.SERVICE_URL}:${process.env.SERVICE_PORT}`,
|
||||
loader: { keepCase: true, enums: String },
|
||||
},
|
||||
});
|
||||
|
||||
await app.startAllMicroservices();
|
||||
await app.listen(process.env.HEALTH_SERVICE_PORT);
|
||||
await app.listen(process.env.HEALTH_SERVICE_PORT as unknown as number);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe';
|
||||
import { CreateConfigurationCommand } from '../../commands/create-configuration.command';
|
||||
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
|
||||
import { UpdateConfigurationCommand } from '../../commands/update-configuration.command';
|
||||
import { CreateConfigurationRequest } from '../../domain/dtos/create-configuration.request';
|
||||
import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request';
|
||||
import { FindConfigurationByUuidRequest } from '../../domain/dtos/find-configuration-by-uuid.request';
|
||||
import { UpdateConfigurationRequest } from '../../domain/dtos/update-configuration.request';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
|
||||
import { FindConfigurationByUuidQuery } from '../../queries/find-configuration-by-uuid.query';
|
||||
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
|
||||
import { ConfigurationPresenter } from './configuration.presenter';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class ConfigurationController {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly queryBus: QueryBus,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'FindAll')
|
||||
async findAll(
|
||||
data: FindAllConfigurationsRequest,
|
||||
): Promise<ICollection<Configuration>> {
|
||||
const configurationCollection = await this.queryBus.execute(
|
||||
new FindAllConfigurationsQuery(data),
|
||||
);
|
||||
return Promise.resolve({
|
||||
data: configurationCollection.data.map((configuration: Configuration) =>
|
||||
this.mapper.map(configuration, Configuration, ConfigurationPresenter),
|
||||
),
|
||||
total: configurationCollection.total,
|
||||
});
|
||||
}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'FindOneByUuid')
|
||||
async findOneByUuid(
|
||||
data: FindConfigurationByUuidRequest,
|
||||
): Promise<ConfigurationPresenter> {
|
||||
try {
|
||||
const configuration = await this.queryBus.execute(
|
||||
new FindConfigurationByUuidQuery(data),
|
||||
);
|
||||
return this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationPresenter,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'Configuration not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'Create')
|
||||
async createConfiguration(
|
||||
data: CreateConfigurationRequest,
|
||||
): Promise<ConfigurationPresenter> {
|
||||
try {
|
||||
const configuration = await this.commandBus.execute(
|
||||
new CreateConfigurationCommand(data),
|
||||
);
|
||||
return this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationPresenter,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('Already exists')) {
|
||||
throw new RpcException({
|
||||
code: 6,
|
||||
message: 'Configuration already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'Update')
|
||||
async updateConfiguration(
|
||||
data: UpdateConfigurationRequest,
|
||||
): Promise<ConfigurationPresenter> {
|
||||
try {
|
||||
const configuration = await this.commandBus.execute(
|
||||
new UpdateConfigurationCommand(data),
|
||||
);
|
||||
|
||||
return this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationPresenter,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('not found')) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'Configuration not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'Delete')
|
||||
async deleteConfiguration(
|
||||
data: FindConfigurationByUuidRequest,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteConfigurationCommand(data.uuid));
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('not found')) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'Configuration not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('ConfigurationService', 'Propagate')
|
||||
async propagate(): Promise<void> {
|
||||
try {
|
||||
await this.queryBus.execute(new PropagateConfigurationsQuery());
|
||||
} catch (e) {
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
|
||||
export class ConfigurationPresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
domain: Domain;
|
||||
|
||||
@AutoMap()
|
||||
key: string;
|
||||
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package configuration;
|
||||
|
||||
service ConfigurationService {
|
||||
rpc FindOneByUuid(ConfigurationByUuid) returns (Configuration);
|
||||
rpc FindAll(ConfigurationFilter) returns (Configurations);
|
||||
rpc Create(Configuration) returns (Configuration);
|
||||
rpc Update(Configuration) returns (Configuration);
|
||||
rpc Delete(ConfigurationByUuid) returns (Empty);
|
||||
rpc Propagate(Empty) returns (Empty);
|
||||
}
|
||||
|
||||
message ConfigurationByUuid {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
message Configuration {
|
||||
string uuid = 1;
|
||||
string domain = 2;
|
||||
string key = 3;
|
||||
string value = 4;
|
||||
}
|
||||
|
||||
message ConfigurationFilter {
|
||||
optional int32 page = 1;
|
||||
optional int32 perPage = 2;
|
||||
}
|
||||
|
||||
message Configurations {
|
||||
repeated Configuration data = 1;
|
||||
int32 total = 2;
|
||||
}
|
||||
|
||||
message Empty {}
|
|
@ -1,13 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
|
||||
export class ConfigurationMessagerPresenter {
|
||||
@AutoMap()
|
||||
domain: Domain;
|
||||
|
||||
@AutoMap()
|
||||
key: string;
|
||||
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigRepository } from '../../../database/domain/configuration.repository';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationRepository extends ConfigRepository<Configuration> {
|
||||
protected model = 'configuration';
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { CreateConfigurationRequest } from '../domain/dtos/create-configuration.request';
|
||||
|
||||
export class CreateConfigurationCommand {
|
||||
readonly createConfigurationRequest: CreateConfigurationRequest;
|
||||
|
||||
constructor(request: CreateConfigurationRequest) {
|
||||
this.createConfigurationRequest = request;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export class DeleteConfigurationCommand {
|
||||
readonly uuid: string;
|
||||
|
||||
constructor(uuid: string) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { UpdateConfigurationRequest } from '../domain/dtos/update-configuration.request';
|
||||
|
||||
export class UpdateConfigurationCommand {
|
||||
readonly updateConfigurationRequest: UpdateConfigurationRequest;
|
||||
|
||||
constructor(request: UpdateConfigurationRequest) {
|
||||
this.updateConfigurationRequest = request;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export const CONFIGURATION_MESSAGE_PUBLISHER = Symbol(
|
||||
'CONFIGURATION_MESSAGE_PUBLISHER',
|
||||
);
|
||||
export const CONFIGURATION_REPOSITORY = Symbol('CONFIGURATION_REPOSITORY');
|
|
@ -0,0 +1,60 @@
|
|||
import { Mapper } from '@mobicoop/ddd-library';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationEntity } from './core/domain/configuration.entity';
|
||||
import {
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel,
|
||||
} from './infrastructure/configuration.repository';
|
||||
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
* Record is an object that is stored in a database,
|
||||
* Entity is an object that is used in application domain layer,
|
||||
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationMapper
|
||||
implements
|
||||
Mapper<
|
||||
ConfigurationEntity,
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel,
|
||||
ConfigurationResponseDto
|
||||
>
|
||||
{
|
||||
toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
|
||||
const copy = entity.getProps();
|
||||
const record: ConfigurationWriteModel = {
|
||||
uuid: entity.id,
|
||||
domain: copy.domain,
|
||||
key: copy.key,
|
||||
value: copy.value,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
|
||||
const entity = new ConfigurationEntity({
|
||||
id: record.uuid,
|
||||
createdAt: new Date(record.createdAt),
|
||||
updatedAt: new Date(record.updatedAt),
|
||||
props: {
|
||||
domain: record.domain,
|
||||
key: record.key,
|
||||
value: record.value,
|
||||
},
|
||||
});
|
||||
return entity;
|
||||
};
|
||||
|
||||
toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
|
||||
const props = entity.getProps();
|
||||
const response = new ConfigurationResponseDto(entity);
|
||||
response.domain = props.domain;
|
||||
response.key = props.key;
|
||||
response.value = props.value;
|
||||
return response;
|
||||
};
|
||||
}
|
|
@ -1,50 +1,73 @@
|
|||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { ConfigurationController } from './adapters/primaries/configuration.controller';
|
||||
import { Messager } from './adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from './adapters/secondaries/configuration.repository';
|
||||
import { CreateConfigurationUseCase } from './domain/usecases/create-configuration.usecase';
|
||||
import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase';
|
||||
import { FindAllConfigurationsUseCase } from './domain/usecases/find-all-configurations.usecase';
|
||||
import { FindConfigurationByUuidUseCase } from './domain/usecases/find-configuration-by-uuid.usecase';
|
||||
import { PropagateConfigurationsUseCase } from './domain/usecases/propagate-configurations.usecase';
|
||||
import { UpdateConfigurationUseCase } from './domain/usecases/update-configuration.usecase';
|
||||
import { ConfigurationProfile } from './mappers/configuration.profile';
|
||||
import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller';
|
||||
import { SetConfigurationGrpcController } from './interface/grpc-controllers/set-configuration.grpc.controller';
|
||||
import { DeleteConfigurationGrpcController } from './interface/grpc-controllers/delete-configuration.grpc.controller';
|
||||
import { PropagateConfigurationsGrpcController } from './interface/grpc-controllers/propagate-configurations.grpc.controller';
|
||||
import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler';
|
||||
import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler';
|
||||
import { SetConfigurationService } from './core/application/commands/set-configuration/set-configuration.service';
|
||||
import { DeleteConfigurationService } from './core/application/commands/delete-configuration/delete-configuration.service';
|
||||
import { GetConfigurationQueryHandler } from './core/application/queries/get-configuration/get-configuration.query-handler';
|
||||
import { ConfigurationMapper } from './configuration.mapper';
|
||||
import {
|
||||
CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
CONFIGURATION_REPOSITORY,
|
||||
} from './configuration.di-tokens';
|
||||
import { ConfigurationRepository } from './infrastructure/configuration.repository';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { PropagateConfigurationsService } from './core/application/commands/propagate-configurations/propagate-configurations.service';
|
||||
|
||||
const grpcControllers = [
|
||||
GetConfigurationGrpcController,
|
||||
SetConfigurationGrpcController,
|
||||
DeleteConfigurationGrpcController,
|
||||
PropagateConfigurationsGrpcController,
|
||||
];
|
||||
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenConfigurationIsSetDomainEventHandler,
|
||||
PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [
|
||||
SetConfigurationService,
|
||||
DeleteConfigurationService,
|
||||
PropagateConfigurationsService,
|
||||
];
|
||||
|
||||
const queryHandlers: Provider[] = [GetConfigurationQueryHandler];
|
||||
|
||||
const mappers: Provider[] = [ConfigurationMapper];
|
||||
|
||||
const repositories: Provider[] = [
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useClass: ConfigurationRepository,
|
||||
},
|
||||
];
|
||||
|
||||
const messagePublishers: Provider[] = [
|
||||
{
|
||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
useExisting: MessageBrokerPublisher,
|
||||
},
|
||||
];
|
||||
const orms: Provider[] = [PrismaService];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
CqrsModule,
|
||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
exchanges: [
|
||||
{
|
||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
||||
type: 'topic',
|
||||
},
|
||||
],
|
||||
uri: configService.get<string>('RMQ_URI'),
|
||||
connectionInitOptions: { wait: false },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
exports: [],
|
||||
controllers: [ConfigurationController],
|
||||
imports: [CqrsModule],
|
||||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
ConfigurationProfile,
|
||||
ConfigurationRepository,
|
||||
Messager,
|
||||
FindAllConfigurationsUseCase,
|
||||
FindConfigurationByUuidUseCase,
|
||||
CreateConfigurationUseCase,
|
||||
UpdateConfigurationUseCase,
|
||||
DeleteConfigurationUseCase,
|
||||
PropagateConfigurationsUseCase,
|
||||
...eventHandlers,
|
||||
...commandHandlers,
|
||||
...queryHandlers,
|
||||
...mappers,
|
||||
...repositories,
|
||||
...messagePublishers,
|
||||
...orms,
|
||||
],
|
||||
exports: [PrismaService, ConfigurationMapper, CONFIGURATION_REPOSITORY],
|
||||
})
|
||||
export class ConfigurationModule {}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteConfigurationCommand extends Command {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
|
||||
constructor(props: CommandProps<DeleteConfigurationCommand>) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DeleteConfigurationCommand } from './delete-configuration.command';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
|
||||
@CommandHandler(DeleteConfigurationCommand)
|
||||
export class DeleteConfigurationService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteConfigurationCommand): Promise<boolean> {
|
||||
const configuration: ConfigurationEntity =
|
||||
await this.configurationRepository.findOne({
|
||||
domain: command.domain,
|
||||
key: command.key,
|
||||
});
|
||||
configuration.delete();
|
||||
const isDeleted: boolean =
|
||||
await this.configurationRepository.delete(configuration);
|
||||
return isDeleted;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class PropagateConfigurationsCommand extends Command {
|
||||
constructor(props: CommandProps<PropagateConfigurationsCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import {
|
||||
CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
CONFIGURATION_REPOSITORY,
|
||||
} from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import { PropagateConfigurationsCommand } from './propagate-configurations.command';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { CONFIGURATION_PROPAGATED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
|
||||
@CommandHandler(PropagateConfigurationsCommand)
|
||||
export class PropagateConfigurationsService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly repository: ConfigurationRepositoryPort,
|
||||
@Inject(CONFIGURATION_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
private readonly configurationMapper: ConfigurationMapper,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const configurationItems: ConfigurationEntity[] =
|
||||
await this.repository.findAll({});
|
||||
this.messagePublisher.publish(
|
||||
CONFIGURATION_PROPAGATED_ROUTING_KEY,
|
||||
JSON.stringify(
|
||||
configurationItems.map((configuration: ConfigurationEntity) =>
|
||||
this.configurationMapper.toResponse(configuration),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class SetConfigurationCommand extends Command {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
|
||||
constructor(props: CommandProps<SetConfigurationCommand>) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
this.value = props.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { SetConfigurationCommand } from './set-configuration.command';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
|
||||
@CommandHandler(SetConfigurationCommand)
|
||||
export class SetConfigurationService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly repository: ConfigurationRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: SetConfigurationCommand): Promise<AggregateID> {
|
||||
try {
|
||||
const existingConfiguration: ConfigurationEntity =
|
||||
await this.repository.findOne({
|
||||
domain: command.domain,
|
||||
key: command.key,
|
||||
});
|
||||
existingConfiguration.update(command);
|
||||
await this.repository.update(
|
||||
existingConfiguration.id,
|
||||
existingConfiguration,
|
||||
);
|
||||
return existingConfiguration.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException) {
|
||||
try {
|
||||
const newConfiguration = ConfigurationEntity.create(command);
|
||||
await this.repository.insert(newConfiguration);
|
||||
return newConfiguration.id;
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationDeletedDomainEvent } from '../../domain/events/configuration-deleted.domain-event';
|
||||
import { ConfigurationDeletedIntegrationEvent } from '../events/configuration-deleted.integration-event';
|
||||
import { CONFIGURATION_DELETED_ROUTING_KEY } from '@src/app.constants';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenConfigurationIsDeletedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(ConfigurationDeletedDomainEvent.name, {
|
||||
async: true,
|
||||
promisify: true,
|
||||
})
|
||||
async handle(event: ConfigurationDeletedDomainEvent): Promise<any> {
|
||||
const configurationDeletedIntegrationEvent =
|
||||
new ConfigurationDeletedIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
domain: event.domain,
|
||||
key: event.key,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
CONFIGURATION_DELETED_ROUTING_KEY,
|
||||
JSON.stringify(configurationDeletedIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { ConfigurationSetIntegrationEvent } from '../events/configuration-set.integration-event';
|
||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationSetDomainEvent } from '../../domain/events/configuration-set.domain-event';
|
||||
import { CONFIGURATION_SET_ROUTING_KEY } from '@src/app.constants';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenConfigurationIsSetDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(ConfigurationSetDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: ConfigurationSetDomainEvent): Promise<any> {
|
||||
const configurationSetIntegrationEvent =
|
||||
new ConfigurationSetIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
domain: event.domain,
|
||||
key: event.key,
|
||||
value: event.value,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
CONFIGURATION_SET_ROUTING_KEY,
|
||||
JSON.stringify(configurationSetIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationDeletedIntegrationEvent extends IntegrationEvent {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
|
||||
constructor(
|
||||
props: IntegrationEventProps<ConfigurationDeletedIntegrationEvent>,
|
||||
) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationSetIntegrationEvent extends IntegrationEvent {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
|
||||
constructor(props: IntegrationEventProps<ConfigurationSetIntegrationEvent>) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
this.value = props.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||
import { ConfigurationEntity } from '../../domain/configuration.entity';
|
||||
|
||||
export type ConfigurationRepositoryPort = RepositoryPort<ConfigurationEntity>;
|
|
@ -0,0 +1,17 @@
|
|||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { GetConfigurationQuery } from './get-configuration.query';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
|
||||
@QueryHandler(GetConfigurationQuery)
|
||||
export class GetConfigurationQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||
) {}
|
||||
async execute(query: GetConfigurationQuery): Promise<ConfigurationEntity> {
|
||||
return await this.configurationRepository.findOne(query);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { QueryBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class GetConfigurationQuery extends QueryBase {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
|
||||
constructor(domain: string, key: string) {
|
||||
super();
|
||||
this.domain = domain;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
ConfigurationProps,
|
||||
CreateConfigurationProps,
|
||||
UpdateConfigurationProps,
|
||||
} from './configuration.types';
|
||||
import { ConfigurationSetDomainEvent } from './events/configuration-set.domain-event';
|
||||
import { ConfigurationDeletedDomainEvent } from './events/configuration-deleted.domain-event';
|
||||
|
||||
export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = (create: CreateConfigurationProps): ConfigurationEntity => {
|
||||
const id = v4();
|
||||
const props: ConfigurationProps = { ...create };
|
||||
const configuration = new ConfigurationEntity({ id, props });
|
||||
configuration.addEvent(
|
||||
new ConfigurationSetDomainEvent({
|
||||
aggregateId: id,
|
||||
domain: props.domain,
|
||||
key: props.key,
|
||||
value: props.value,
|
||||
}),
|
||||
);
|
||||
return configuration;
|
||||
};
|
||||
|
||||
update(props: UpdateConfigurationProps): void {
|
||||
this.props.value = props.value;
|
||||
this.addEvent(
|
||||
new ConfigurationSetDomainEvent({
|
||||
aggregateId: this._id,
|
||||
domain: this.props.domain,
|
||||
key: this.props.key,
|
||||
value: props.value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new ConfigurationDeletedDomainEvent({
|
||||
aggregateId: this.id,
|
||||
domain: this.props.domain,
|
||||
key: this.props.key,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// All properties that a Configuration has
|
||||
export interface ConfigurationProps {
|
||||
domain: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Properties that are needed for a Configuration creation
|
||||
export interface CreateConfigurationProps {
|
||||
domain: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateConfigurationProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export enum Domain {
|
||||
AD = 'AD',
|
||||
MATCHER = 'MATCHER',
|
||||
USER = 'USER',
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationDeletedDomainEvent extends DomainEvent {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
|
||||
constructor(props: DomainEventProps<ConfigurationDeletedDomainEvent>) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationSetDomainEvent extends DomainEvent {
|
||||
readonly domain: string;
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
|
||||
constructor(props: DomainEventProps<ConfigurationSetDomainEvent>) {
|
||||
super(props);
|
||||
this.domain = props.domain;
|
||||
this.key = props.key;
|
||||
this.value = props.value;
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { Domain } from './domain.enum';
|
||||
|
||||
export class CreateConfigurationRequest {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
uuid?: string;
|
||||
|
||||
@IsEnum(Domain)
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
domain: Domain;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
key: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export enum Domain {
|
||||
USER = 'USER',
|
||||
MATCHER = 'MATCHER',
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class FindAllConfigurationsRequest {
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
perPage?: number;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class FindConfigurationByUuidRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
uuid: string;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateConfigurationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { Domain } from '../dtos/domain.enum';
|
||||
|
||||
export class Configuration {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
domain: Domain;
|
||||
|
||||
@AutoMap()
|
||||
key: string;
|
||||
|
||||
@AutoMap()
|
||||
value: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { ConfigurationMessagerPresenter } from '../../adapters/secondaries/configuration-messager.presenter';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { CreateConfigurationCommand } from '../../commands/create-configuration.command';
|
||||
import { CreateConfigurationRequest } from '../dtos/create-configuration.request';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@CommandHandler(CreateConfigurationCommand)
|
||||
export class CreateConfigurationUseCase {
|
||||
constructor(
|
||||
private readonly repository: ConfigurationRepository,
|
||||
private readonly messager: Messager,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
command: CreateConfigurationCommand,
|
||||
): Promise<Configuration> => {
|
||||
const entity = this.mapper.map(
|
||||
command.createConfigurationRequest,
|
||||
CreateConfigurationRequest,
|
||||
Configuration,
|
||||
);
|
||||
|
||||
try {
|
||||
const configuration = await this.repository.create(entity);
|
||||
this.messager.publish(
|
||||
'configuration.create',
|
||||
JSON.stringify(
|
||||
this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationMessagerPresenter,
|
||||
),
|
||||
),
|
||||
);
|
||||
this.messager.publish(
|
||||
'logging.configuration.create.info',
|
||||
JSON.stringify(configuration),
|
||||
);
|
||||
return configuration;
|
||||
} catch (error) {
|
||||
let key = 'logging.configuration.create.crit';
|
||||
if (error.message.includes('Already exists')) {
|
||||
key = 'logging.configuration.create.warning';
|
||||
}
|
||||
this.messager.publish(
|
||||
key,
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { ConfigurationMessagerPresenter } from '../../adapters/secondaries/configuration-messager.presenter';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@CommandHandler(DeleteConfigurationCommand)
|
||||
export class DeleteConfigurationUseCase {
|
||||
constructor(
|
||||
private readonly repository: ConfigurationRepository,
|
||||
private readonly messager: Messager,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
command: DeleteConfigurationCommand,
|
||||
): Promise<Configuration> => {
|
||||
try {
|
||||
const configuration = await this.repository.delete(command.uuid);
|
||||
this.messager.publish(
|
||||
'configuration.delete',
|
||||
JSON.stringify(
|
||||
this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationMessagerPresenter,
|
||||
),
|
||||
),
|
||||
);
|
||||
this.messager.publish(
|
||||
'logging.configuration.delete.info',
|
||||
JSON.stringify({ uuid: configuration.uuid }),
|
||||
);
|
||||
return configuration;
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.configuration.delete.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@QueryHandler(FindAllConfigurationsQuery)
|
||||
export class FindAllConfigurationsUseCase {
|
||||
constructor(private readonly repository: ConfigurationRepository) {}
|
||||
|
||||
execute = async (
|
||||
findAllConfigurationsQuery: FindAllConfigurationsQuery,
|
||||
): Promise<ICollection<Configuration>> =>
|
||||
this.repository.findAll(
|
||||
findAllConfigurationsQuery.page,
|
||||
findAllConfigurationsQuery.perPage,
|
||||
);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import { NotFoundException } from '@nestjs/common';
|
||||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { FindConfigurationByUuidQuery } from '../../queries/find-configuration-by-uuid.query';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@QueryHandler(FindConfigurationByUuidQuery)
|
||||
export class FindConfigurationByUuidUseCase {
|
||||
constructor(
|
||||
private readonly repository: ConfigurationRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
findConfigurationByUuid: FindConfigurationByUuidQuery,
|
||||
): Promise<Configuration> => {
|
||||
try {
|
||||
const configuration = await this.repository.findOneByUuid(
|
||||
findConfigurationByUuid.uuid,
|
||||
);
|
||||
if (!configuration) throw new NotFoundException();
|
||||
return configuration;
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.configuration.read.warning',
|
||||
JSON.stringify({
|
||||
query: findConfigurationByUuid,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { ConfigurationMessagerPresenter } from '../../adapters/secondaries/configuration-messager.presenter';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@QueryHandler(PropagateConfigurationsQuery)
|
||||
export class PropagateConfigurationsUseCase {
|
||||
constructor(
|
||||
private readonly repository: ConfigurationRepository,
|
||||
private readonly messager: Messager,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
propagateConfigurationsQuery: PropagateConfigurationsQuery,
|
||||
) => {
|
||||
try {
|
||||
const configurations = await this.repository.findAll(1, 999999);
|
||||
this.messager.publish(
|
||||
'configuration.propagate',
|
||||
JSON.stringify(
|
||||
configurations.data.map((configuration) =>
|
||||
this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationMessagerPresenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
this.messager.publish('logging.configuration.update.info', 'propagation');
|
||||
} catch (error) {
|
||||
this.messager.publish('logging.configuration.update.crit', 'propagation');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { ConfigurationMessagerPresenter } from '../../adapters/secondaries/configuration-messager.presenter';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { UpdateConfigurationCommand } from '../../commands/update-configuration.command';
|
||||
import { UpdateConfigurationRequest } from '../dtos/update-configuration.request';
|
||||
import { Configuration } from '../entities/configuration';
|
||||
|
||||
@CommandHandler(UpdateConfigurationCommand)
|
||||
export class UpdateConfigurationUseCase {
|
||||
constructor(
|
||||
private readonly repository: ConfigurationRepository,
|
||||
private readonly messager: Messager,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
command: UpdateConfigurationCommand,
|
||||
): Promise<Configuration> => {
|
||||
const entity = this.mapper.map(
|
||||
command.updateConfigurationRequest,
|
||||
UpdateConfigurationRequest,
|
||||
Configuration,
|
||||
);
|
||||
|
||||
try {
|
||||
const configuration = await this.repository.update(
|
||||
command.updateConfigurationRequest.uuid,
|
||||
entity,
|
||||
);
|
||||
this.messager.publish(
|
||||
'configuration.update',
|
||||
JSON.stringify(
|
||||
this.mapper.map(
|
||||
configuration,
|
||||
Configuration,
|
||||
ConfigurationMessagerPresenter,
|
||||
),
|
||||
),
|
||||
);
|
||||
this.messager.publish(
|
||||
'logging.configuration.update.info',
|
||||
JSON.stringify(command.updateConfigurationRequest),
|
||||
);
|
||||
return configuration;
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.configuration.update.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
LoggerBase,
|
||||
MessagePublisherPort,
|
||||
PrismaRepositoryBase,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { SERVICE_NAME } from '@src/app.constants';
|
||||
import { ConfigurationEntity } from '../core/domain/configuration.entity';
|
||||
import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '../configuration.di-tokens';
|
||||
import { ConfigurationMapper } from '../configuration.mapper';
|
||||
|
||||
export type ConfigurationBaseModel = {
|
||||
uuid: string;
|
||||
domain: string;
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ConfigurationReadModel = ConfigurationBaseModel & {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type ConfigurationWriteModel = ConfigurationBaseModel;
|
||||
|
||||
/**
|
||||
* Repository is used for retrieving/saving domain entities
|
||||
* */
|
||||
@Injectable()
|
||||
export class ConfigurationRepository
|
||||
extends PrismaRepositoryBase<
|
||||
ConfigurationEntity,
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel
|
||||
>
|
||||
implements ConfigurationRepositoryPort
|
||||
{
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
mapper: ConfigurationMapper,
|
||||
eventEmitter: EventEmitter2,
|
||||
@Inject(CONFIGURATION_MESSAGE_PUBLISHER)
|
||||
protected readonly messagePublisher: MessagePublisherPort,
|
||||
) {
|
||||
super(
|
||||
prisma.configuration,
|
||||
prisma,
|
||||
mapper,
|
||||
eventEmitter,
|
||||
new LoggerBase({
|
||||
logger: new Logger(ConfigurationRepository.name),
|
||||
domain: SERVICE_NAME,
|
||||
messagePublisher,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationResponseDto extends ResponseBase {
|
||||
domain: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package configuration;
|
||||
|
||||
service ConfigurationService {
|
||||
rpc Get(ConfigurationByDomainKey) returns (Configuration);
|
||||
rpc Set(Configuration) returns (ConfigurationId);
|
||||
rpc Delete(ConfigurationByDomainKey) returns (Empty);
|
||||
rpc Propagate(Empty) returns (Empty);
|
||||
}
|
||||
message ConfigurationId {
|
||||
string id = 1;
|
||||
}
|
||||
message ConfigurationByDomainKey {
|
||||
string domain = 1;
|
||||
string key = 2;
|
||||
}
|
||||
|
||||
message Configuration {
|
||||
string id = 1;
|
||||
string domain = 2;
|
||||
string key = 3;
|
||||
string value = 4;
|
||||
}
|
||||
|
||||
message Empty {}
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DeleteConfigurationRequestDto } from './dtos/delete-configuration.request.dto';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { DeleteConfigurationCommand } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.command';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class DeleteConfigurationGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Delete')
|
||||
async delete(data: DeleteConfigurationRequestDto): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteConfigurationCommand(data));
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: error.message,
|
||||
});
|
||||
if (error instanceof DatabaseErrorException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.INTERNAL,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteConfigurationRequestDto {
|
||||
@IsEnum(Domain)
|
||||
@IsNotEmpty()
|
||||
domain: Domain;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
key: string;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class GetConfigurationRequestDto {
|
||||
@IsEnum(Domain)
|
||||
@IsNotEmpty()
|
||||
domain: Domain;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
key: string;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class SetConfigurationRequestDto {
|
||||
@IsEnum(Domain)
|
||||
@IsNotEmpty()
|
||||
domain: Domain;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
key: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
value: string;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
import { GetConfigurationRequestDto } from './dtos/get-configuration.request.dto';
|
||||
import { ConfigurationResponseDto } from '../dtos/configuration.response.dto';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class GetConfigurationGrpcController {
|
||||
constructor(
|
||||
protected readonly mapper: ConfigurationMapper,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Get')
|
||||
async get(
|
||||
data: GetConfigurationRequestDto,
|
||||
): Promise<ConfigurationResponseDto> {
|
||||
try {
|
||||
const configuration: ConfigurationEntity = await this.queryBus.execute(
|
||||
new GetConfigurationQuery(data.domain, data.key),
|
||||
);
|
||||
return this.mapper.toResponse(configuration);
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundException) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { PropagateConfigurationsCommand } from '@modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.command';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class PropagateConfigurationsGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Propagate')
|
||||
async propagate(): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(
|
||||
new PropagateConfigurationsCommand({ id: v4() }),
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { SetConfigurationRequestDto } from './dtos/set-configuration.request.dto';
|
||||
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class SetConfigurationGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Set')
|
||||
async set(
|
||||
setConfigurationRequestDto: SetConfigurationRequestDto,
|
||||
): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new SetConfigurationCommand(setConfigurationRequestDto),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { createMap, forMember, ignore, Mapper } from '@automapper/core';
|
||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationPresenter } from '../adapters/primaries/configuration.presenter';
|
||||
import { ConfigurationMessagerPresenter } from '../adapters/secondaries/configuration-messager.presenter';
|
||||
import { CreateConfigurationRequest } from '../domain/dtos/create-configuration.request';
|
||||
import { UpdateConfigurationRequest } from '../domain/dtos/update-configuration.request';
|
||||
import { Configuration } from '../domain/entities/configuration';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationProfile extends AutomapperProfile {
|
||||
constructor(@InjectMapper() mapper: Mapper) {
|
||||
super(mapper);
|
||||
}
|
||||
|
||||
override get profile() {
|
||||
return (mapper: any) => {
|
||||
createMap(mapper, Configuration, ConfigurationPresenter);
|
||||
|
||||
createMap(mapper, CreateConfigurationRequest, Configuration);
|
||||
|
||||
createMap(
|
||||
mapper,
|
||||
UpdateConfigurationRequest,
|
||||
Configuration,
|
||||
forMember((dest) => dest.uuid, ignore()),
|
||||
);
|
||||
|
||||
createMap(mapper, Configuration, ConfigurationMessagerPresenter);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { FindAllConfigurationsRequest } from '../domain/dtos/find-all-configurations.request';
|
||||
|
||||
export class FindAllConfigurationsQuery {
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
||||
constructor(findAllConfigurationsRequest?: FindAllConfigurationsRequest) {
|
||||
this.page = findAllConfigurationsRequest?.page ?? 1;
|
||||
this.perPage = findAllConfigurationsRequest?.perPage ?? 10;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { FindConfigurationByUuidRequest } from '../domain/dtos/find-configuration-by-uuid.request';
|
||||
|
||||
export class FindConfigurationByUuidQuery {
|
||||
readonly uuid: string;
|
||||
|
||||
constructor(findConfigurationByUuidRequest: FindConfigurationByUuidRequest) {
|
||||
this.uuid = findConfigurationByUuidRequest.uuid;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export class PropagateConfigurationsQuery {}
|
|
@ -1,36 +1,88 @@
|
|||
import { TestingModule, Test } from '@nestjs/testing';
|
||||
import { DatabaseModule } from '../../../database/database.module';
|
||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import {
|
||||
CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
CONFIGURATION_REPOSITORY,
|
||||
} from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import {
|
||||
CreateConfigurationProps,
|
||||
Domain,
|
||||
} from '@modules/configuration/core/domain/configuration.types';
|
||||
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
|
||||
import { PrismaService } from '@modules/configuration/infrastructure/prisma.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
describe('ConfigurationRepository', () => {
|
||||
describe('Configuration Repository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let configurationRepository: ConfigurationRepository;
|
||||
|
||||
const executeInsertCommand = async (table: string, object: any) => {
|
||||
const command = `INSERT INTO "${table}" ("${Object.keys(object).join(
|
||||
'","',
|
||||
)}") VALUES ('${Object.values(object).join("','")}')`;
|
||||
await prismaService.$executeRawUnsafe(command);
|
||||
};
|
||||
const getSeed = (index: number, uuid: string): string => {
|
||||
return `${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const baseUuid = {
|
||||
uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
|
||||
};
|
||||
|
||||
const createConfigurations = async (nbToCreate = 10) => {
|
||||
for (let i = 0; i < nbToCreate; i++) {
|
||||
await prismaService.configuration.create({
|
||||
data: {
|
||||
domain: Domain.USER,
|
||||
key: `key-${i}`,
|
||||
value: `key-${i}`,
|
||||
},
|
||||
});
|
||||
const configurationToCreate = {
|
||||
uuid: getSeed(i, baseUuid.uuid),
|
||||
domain: Domain.AD,
|
||||
key: `key${i}`,
|
||||
value: `value${i}`,
|
||||
createdAt: '2023-07-24 13:07:05.000',
|
||||
updatedAt: '2023-07-24 13:07:05.000',
|
||||
};
|
||||
configurationToCreate.uuid = getSeed(i, baseUuid.uuid);
|
||||
await executeInsertCommand('configuration', configurationToCreate);
|
||||
}
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [DatabaseModule],
|
||||
providers: [ConfigurationRepository, PrismaService],
|
||||
}).compile();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
EventEmitterModule.forRoot(),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
ConfigurationMapper,
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useClass: ConfigurationRepository,
|
||||
},
|
||||
{
|
||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
],
|
||||
})
|
||||
// disable logging
|
||||
.setLogger(mockLogger)
|
||||
.compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
configurationRepository = module.get<ConfigurationRepository>(
|
||||
ConfigurationRepository,
|
||||
CONFIGURATION_REPOSITORY,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -42,78 +94,23 @@ describe('ConfigurationRepository', () => {
|
|||
await prismaService.configuration.deleteMany();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an empty data array', async () => {
|
||||
const res = await configurationRepository.findAll();
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a data array with 8 configurations', async () => {
|
||||
await createConfigurations(8);
|
||||
const configurations = await configurationRepository.findAll();
|
||||
expect(configurations.data.length).toBe(8);
|
||||
expect(configurations.total).toBe(8);
|
||||
});
|
||||
|
||||
it('should return a data array limited to 10 configurations', async () => {
|
||||
await createConfigurations(20);
|
||||
const configurations = await configurationRepository.findAll();
|
||||
expect(configurations.data.length).toBe(10);
|
||||
expect(configurations.total).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneByUuid', () => {
|
||||
it('should return a configuration', async () => {
|
||||
const configurationToFind = await prismaService.configuration.create({
|
||||
data: {
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
});
|
||||
|
||||
const configuration = await configurationRepository.findOneByUuid(
|
||||
configurationToFind.uuid,
|
||||
);
|
||||
expect(configuration.uuid).toBe(configurationToFind.uuid);
|
||||
});
|
||||
|
||||
it('should return null', async () => {
|
||||
const configuration = await configurationRepository.findOneByUuid(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
);
|
||||
expect(configuration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a configuration according to its domain and key', async () => {
|
||||
const configurationToFind = await prismaService.configuration.create({
|
||||
data: {
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
it('should return a configuration', async () => {
|
||||
await createConfigurations(1);
|
||||
const result = await configurationRepository.findOne({
|
||||
domain: Domain.AD,
|
||||
key: 'key0',
|
||||
});
|
||||
|
||||
const configuration = await configurationRepository.findOne({
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
});
|
||||
|
||||
expect(configuration.uuid).toBe(configurationToFind.uuid);
|
||||
expect(result.getProps().value).toBe('value0');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null with unknown domain and key', async () => {
|
||||
const configuration = await configurationRepository.findOne({
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
});
|
||||
expect(configuration).toBeNull();
|
||||
describe('findAll', () => {
|
||||
it('should return all configurations', async () => {
|
||||
await createConfigurations(10);
|
||||
const configurations: ConfigurationEntity[] =
|
||||
await configurationRepository.findAll({});
|
||||
expect(configurations).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -121,75 +118,57 @@ describe('ConfigurationRepository', () => {
|
|||
it('should create a configuration', async () => {
|
||||
const beforeCount = await prismaService.configuration.count();
|
||||
|
||||
const configurationToCreate: Configuration = new Configuration();
|
||||
configurationToCreate.domain = Domain.USER;
|
||||
configurationToCreate.key = 'key1';
|
||||
configurationToCreate.value = 'value1';
|
||||
const configuration = await configurationRepository.create(
|
||||
configurationToCreate,
|
||||
);
|
||||
const createConfigurationProps: CreateConfigurationProps = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
};
|
||||
|
||||
const configurationToCreate: ConfigurationEntity =
|
||||
ConfigurationEntity.create(createConfigurationProps);
|
||||
await configurationRepository.insert(configurationToCreate);
|
||||
|
||||
const afterCount = await prismaService.configuration.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
expect(configuration.uuid).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update configuration value', async () => {
|
||||
const configurationToUpdate = await prismaService.configuration.create({
|
||||
data: {
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
});
|
||||
|
||||
const toUpdate: Configuration = new Configuration();
|
||||
toUpdate.value = 'value2';
|
||||
const updatedConfiguration = await configurationRepository.update(
|
||||
configurationToUpdate.uuid,
|
||||
toUpdate,
|
||||
it('should update a configuration', async () => {
|
||||
await createConfigurations(1);
|
||||
const configurationToUpdate: ConfigurationEntity =
|
||||
await configurationRepository.findOne({
|
||||
domain: Domain.AD,
|
||||
key: 'key0',
|
||||
});
|
||||
configurationToUpdate.update({ value: 'newValue' });
|
||||
await configurationRepository.update(
|
||||
configurationToUpdate.id,
|
||||
configurationToUpdate,
|
||||
);
|
||||
|
||||
expect(updatedConfiguration.uuid).toBe(configurationToUpdate.uuid);
|
||||
expect(updatedConfiguration.value).toBe('value2');
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
const toUpdate: Configuration = new Configuration();
|
||||
toUpdate.key = 'updated';
|
||||
|
||||
await expect(
|
||||
configurationRepository.update(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
toUpdate,
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
const result: ConfigurationEntity = await configurationRepository.findOne(
|
||||
{
|
||||
domain: Domain.AD,
|
||||
key: 'key0',
|
||||
},
|
||||
);
|
||||
expect(result.getProps().value).toBe('newValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a configuration', async () => {
|
||||
const configurationToRemove = await prismaService.configuration.create({
|
||||
data: {
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
});
|
||||
|
||||
await configurationRepository.delete(configurationToRemove.uuid);
|
||||
|
||||
const count = await prismaService.configuration.count();
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
await expect(
|
||||
configurationRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
await createConfigurations(10);
|
||||
const beforeCount = await prismaService.configuration.count();
|
||||
const configurationToDelete: ConfigurationEntity =
|
||||
await configurationRepository.findOne({
|
||||
domain: Domain.AD,
|
||||
key: 'key4',
|
||||
});
|
||||
await configurationRepository.delete(configurationToDelete);
|
||||
const afterCount = await prismaService.configuration.count();
|
||||
expect(afterCount - beforeCount).toBe(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import {
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel,
|
||||
} from '@modules/configuration/infrastructure/configuration.repository';
|
||||
import { ConfigurationResponseDto } from '@modules/configuration/interface/dtos/configuration.response.dto';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
const now = new Date('2023-06-21 06:00:00');
|
||||
const configurationEntity: ConfigurationEntity = new ConfigurationEntity({
|
||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
props: {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const configurationReadModel: ConfigurationReadModel = {
|
||||
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
domain: 'AD',
|
||||
key: 'seatsProposed',
|
||||
value: '4',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
describe('Configuration Mapper', () => {
|
||||
let configurationMapper: ConfigurationMapper;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [ConfigurationMapper],
|
||||
}).compile();
|
||||
configurationMapper = module.get<ConfigurationMapper>(ConfigurationMapper);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(configurationMapper).toBeDefined();
|
||||
});
|
||||
|
||||
it('should map domain entity to persistence data', async () => {
|
||||
const mapped: ConfigurationWriteModel =
|
||||
configurationMapper.toPersistence(configurationEntity);
|
||||
expect(mapped.value).toBe('3');
|
||||
});
|
||||
|
||||
it('should map persisted data to domain entity', async () => {
|
||||
const mapped: ConfigurationEntity = configurationMapper.toDomain(
|
||||
configurationReadModel,
|
||||
);
|
||||
expect(mapped.getProps().value).toBe('4');
|
||||
});
|
||||
|
||||
it('should map domain entity to response', async () => {
|
||||
const mapped: ConfigurationResponseDto =
|
||||
configurationMapper.toResponse(configurationEntity);
|
||||
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import {
|
||||
CreateConfigurationProps,
|
||||
Domain,
|
||||
UpdateConfigurationProps,
|
||||
} from '@modules/configuration/core/domain/configuration.types';
|
||||
import { ConfigurationDeletedDomainEvent } from '@modules/configuration/core/domain/events/configuration-deleted.domain-event';
|
||||
import { ConfigurationSetDomainEvent } from '@modules/configuration/core/domain/events/configuration-set.domain-event';
|
||||
|
||||
const createConfigurationProps: CreateConfigurationProps = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
};
|
||||
|
||||
const updateConfigurationProps: UpdateConfigurationProps = {
|
||||
value: '2',
|
||||
};
|
||||
|
||||
describe('Configuration entity create', () => {
|
||||
it('should create a new configuration entity', async () => {
|
||||
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
|
||||
createConfigurationProps,
|
||||
);
|
||||
expect(configurationEntity.id.length).toBe(36);
|
||||
expect(configurationEntity.getProps().value).toBe('3');
|
||||
expect(configurationEntity.domainEvents.length).toBe(1);
|
||||
expect(configurationEntity.domainEvents[0]).toBeInstanceOf(
|
||||
ConfigurationSetDomainEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration entity update', () => {
|
||||
it('should update a configuration entity', async () => {
|
||||
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
|
||||
createConfigurationProps,
|
||||
);
|
||||
configurationEntity.update(updateConfigurationProps);
|
||||
expect(configurationEntity.getProps().value).toBe('2');
|
||||
// 2 events because ConfigurationEntity.create sends a ConfigurationSetDomainEvent
|
||||
expect(configurationEntity.domainEvents.length).toBe(2);
|
||||
expect(configurationEntity.domainEvents[1]).toBeInstanceOf(
|
||||
ConfigurationSetDomainEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration entity delete', () => {
|
||||
it('should delete a configuration entity', async () => {
|
||||
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
|
||||
createConfigurationProps,
|
||||
);
|
||||
configurationEntity.delete();
|
||||
// 2 events because ConfigurationEntity.create sends a ConfigurationSetDomainEvent
|
||||
expect(configurationEntity.domainEvents.length).toBe(2);
|
||||
expect(configurationEntity.domainEvents[1]).toBeInstanceOf(
|
||||
ConfigurationDeletedDomainEvent,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { DeleteConfigurationCommand } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.command';
|
||||
import { DeleteConfigurationService } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.service';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { DeleteConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const deleteConfigurationRequest: DeleteConfigurationRequestDto = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
};
|
||||
|
||||
const mockConfigurationEntity = {
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findOne: jest.fn().mockImplementation(() => mockConfigurationEntity),
|
||||
delete: jest.fn().mockImplementationOnce(() => true),
|
||||
};
|
||||
|
||||
describe('Delete Configuration Service', () => {
|
||||
let deleteConfigurationService: DeleteConfigurationService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
DeleteConfigurationService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteConfigurationService = module.get<DeleteConfigurationService>(
|
||||
DeleteConfigurationService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteConfigurationService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const deleteConfigurationCommand = new DeleteConfigurationCommand(
|
||||
deleteConfigurationRequest,
|
||||
);
|
||||
it('should delete a configuration item', async () => {
|
||||
const result: boolean = await deleteConfigurationService.execute(
|
||||
deleteConfigurationCommand,
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
|
||||
|
||||
const now = new Date('2023-06-21 06:00:00');
|
||||
const configuration: ConfigurationEntity = new ConfigurationEntity({
|
||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
props: {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findOne: jest.fn().mockImplementation(() => configuration),
|
||||
};
|
||||
|
||||
describe('Get Configuration Query Handler', () => {
|
||||
let getConfigurationQueryHandler: GetConfigurationQueryHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
GetConfigurationQueryHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
getConfigurationQueryHandler = module.get<GetConfigurationQueryHandler>(
|
||||
GetConfigurationQueryHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(getConfigurationQueryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return a configuration item', async () => {
|
||||
const getConfigurationQuery = new GetConfigurationQuery(
|
||||
Domain.AD,
|
||||
'seatsProposed',
|
||||
);
|
||||
const configuration: ConfigurationEntity =
|
||||
await getConfigurationQueryHandler.execute(getConfigurationQuery);
|
||||
expect(configuration.getProps().value).toBe('3');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import {
|
||||
CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
CONFIGURATION_REPOSITORY,
|
||||
} from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { PropagateConfigurationsService } from '@modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.service';
|
||||
import { CONFIGURATION_PROPAGATED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const configurationEntities = [
|
||||
new ConfigurationEntity({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
props: {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
},
|
||||
createdAt: new Date('2023-10-23T07:00:00Z'),
|
||||
updatedAt: new Date('2023-10-23T07:00:00Z'),
|
||||
}),
|
||||
new ConfigurationEntity({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8db',
|
||||
props: {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsRequested',
|
||||
value: '1',
|
||||
},
|
||||
createdAt: new Date('2023-10-23T07:00:00Z'),
|
||||
updatedAt: new Date('2023-10-23T07:00:00Z'),
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfigurationMapper = {
|
||||
toResponse: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
domain: Domain.AD,
|
||||
key: 'seatsRequested',
|
||||
value: '1',
|
||||
})),
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findAll: jest.fn().mockImplementationOnce(() => configurationEntities),
|
||||
};
|
||||
|
||||
describe('Propagate Configurations Service', () => {
|
||||
let propagateConfigurationsService: PropagateConfigurationsService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
{
|
||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
{
|
||||
provide: ConfigurationMapper,
|
||||
useValue: mockConfigurationMapper,
|
||||
},
|
||||
PropagateConfigurationsService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
propagateConfigurationsService = module.get<PropagateConfigurationsService>(
|
||||
PropagateConfigurationsService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(propagateConfigurationsService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should propagate configuration items', async () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
await propagateConfigurationsService.execute();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
CONFIGURATION_PROPAGATED_ROUTING_KEY,
|
||||
'[{"domain":"AD","key":"seatsProposed","value":"3"},{"domain":"AD","key":"seatsRequested","value":"1"}]',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
|
||||
import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from '@modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { ConfigurationDeletedDomainEvent } from '@modules/configuration/core/domain/events/configuration-deleted.domain-event';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CONFIGURATION_DELETED_ROUTING_KEY } from '@src/app.constants';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when configuration is deleted domain event handler', () => {
|
||||
let publishMessageWhenConfigurationIsDeletedDomainEventHandler: PublishMessageWhenConfigurationIsDeletedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenConfigurationIsDeletedDomainEventHandler =
|
||||
module.get<PublishMessageWhenConfigurationIsDeletedDomainEventHandler>(
|
||||
PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const configurationDeletedDomainEvent: ConfigurationDeletedDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
};
|
||||
publishMessageWhenConfigurationIsDeletedDomainEventHandler.handle(
|
||||
configurationDeletedDomainEvent,
|
||||
);
|
||||
expect(
|
||||
publishMessageWhenConfigurationIsDeletedDomainEventHandler,
|
||||
).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
CONFIGURATION_DELETED_ROUTING_KEY,
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"domain":"AD","key":"seatsProposed"}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
|
||||
import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from '@modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { ConfigurationSetDomainEvent } from '@modules/configuration/core/domain/events/configuration-set.domain-event';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CONFIGURATION_SET_ROUTING_KEY } from '@src/app.constants';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when configuration is set domain event handler', () => {
|
||||
let publishMessageWhenConfigurationIsSetDomainEventHandler: PublishMessageWhenConfigurationIsSetDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenConfigurationIsSetDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenConfigurationIsSetDomainEventHandler =
|
||||
module.get<PublishMessageWhenConfigurationIsSetDomainEventHandler>(
|
||||
PublishMessageWhenConfigurationIsSetDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const configurationSetDomainEvent: ConfigurationSetDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
};
|
||||
publishMessageWhenConfigurationIsSetDomainEventHandler.handle(
|
||||
configurationSetDomainEvent,
|
||||
);
|
||||
expect(
|
||||
publishMessageWhenConfigurationIsSetDomainEventHandler,
|
||||
).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
CONFIGURATION_SET_ROUTING_KEY,
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"domain":"AD","key":"seatsProposed","value":"3"}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
|
||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
|
||||
const setConfigurationRequest: SetConfigurationRequestDto = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
};
|
||||
|
||||
const existingConfigurationEntity = new ConfigurationEntity({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
props: {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '2',
|
||||
},
|
||||
createdAt: new Date('2023-10-23T07:00:00Z'),
|
||||
updatedAt: new Date('2023-10-23T07:00:00Z'),
|
||||
});
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => existingConfigurationEntity)
|
||||
.mockImplementationOnce(() => existingConfigurationEntity),
|
||||
insert: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
update: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Set Configuration Service', () => {
|
||||
let setConfigurationService: SetConfigurationService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CONFIGURATION_REPOSITORY,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
SetConfigurationService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
setConfigurationService = module.get<SetConfigurationService>(
|
||||
SetConfigurationService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(setConfigurationService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const setConfigurationCommand = new SetConfigurationCommand(
|
||||
setConfigurationRequest,
|
||||
);
|
||||
it('should create a new configuration item', async () => {
|
||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
const result: AggregateID = await setConfigurationService.execute(
|
||||
setConfigurationCommand,
|
||||
);
|
||||
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||
});
|
||||
it('should throw an error if something bad happens on configuration item creation', async () => {
|
||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
setConfigurationService.execute(setConfigurationCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
it('should update an existing configuration item', async () => {
|
||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
const result: AggregateID = await setConfigurationService.execute(
|
||||
setConfigurationCommand,
|
||||
);
|
||||
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||
});
|
||||
it('should throw an error if something bad happens on configuration item update', async () => {
|
||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
setConfigurationService.execute(setConfigurationCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { CreateConfigurationCommand } from '../../commands/create-configuration.command';
|
||||
import { CreateConfigurationRequest } from '../../domain/dtos/create-configuration.request';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { CreateConfigurationUseCase } from '../../domain/usecases/create-configuration.usecase';
|
||||
import { ConfigurationProfile } from '../../mappers/configuration.profile';
|
||||
|
||||
const newConfigurationRequest: CreateConfigurationRequest = {
|
||||
domain: Domain.USER,
|
||||
key: 'minAge',
|
||||
value: '16',
|
||||
};
|
||||
const newConfigurationCommand: CreateConfigurationCommand =
|
||||
new CreateConfigurationCommand(newConfigurationRequest);
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
...newConfigurationRequest,
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
});
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Already exists');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
describe('CreateConfigurationUseCase', () => {
|
||||
let createConfigurationUseCase: CreateConfigurationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
CreateConfigurationUseCase,
|
||||
ConfigurationProfile,
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createConfigurationUseCase = module.get<CreateConfigurationUseCase>(
|
||||
CreateConfigurationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createConfigurationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should create and return a new configuration', async () => {
|
||||
jest.spyOn(mockMessager, 'publish');
|
||||
const newConfiguration: Configuration =
|
||||
await createConfigurationUseCase.execute(newConfigurationCommand);
|
||||
|
||||
expect(newConfiguration.key).toBe(newConfigurationRequest.key);
|
||||
expect(newConfiguration.uuid).toBeDefined();
|
||||
expect(mockMessager.publish).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should throw an error if configuration already exists', async () => {
|
||||
await expect(
|
||||
createConfigurationUseCase.execute(newConfigurationCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { DeleteConfigurationUseCase } from '../../domain/usecases/delete-configuration.usecase';
|
||||
import { ConfigurationProfile } from '../../mappers/configuration.profile';
|
||||
|
||||
const mockConfigurations: ICollection<Configuration> = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
||||
domain: Domain.USER,
|
||||
key: 'key2',
|
||||
value: 'value2',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
||||
domain: Domain.USER,
|
||||
key: 'key3',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
delete: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((uuid: string) => {
|
||||
let savedConfiguration = {};
|
||||
mockConfigurations.data.forEach((configuration, index) => {
|
||||
if (configuration.uuid === uuid) {
|
||||
savedConfiguration = { ...configuration };
|
||||
mockConfigurations.data.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return savedConfiguration;
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('DeleteConfigurationUseCase', () => {
|
||||
let deleteConfigurationUseCase: DeleteConfigurationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
DeleteConfigurationUseCase,
|
||||
ConfigurationProfile,
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteConfigurationUseCase = module.get<DeleteConfigurationUseCase>(
|
||||
DeleteConfigurationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteConfigurationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should delete a configuration', async () => {
|
||||
jest.spyOn(mockMessager, 'publish');
|
||||
const savedUuid = mockConfigurations.data[0].uuid;
|
||||
const deleteConfigurationCommand = new DeleteConfigurationCommand(
|
||||
savedUuid,
|
||||
);
|
||||
await deleteConfigurationUseCase.execute(deleteConfigurationCommand);
|
||||
|
||||
const deletedConfiguration = mockConfigurations.data.find(
|
||||
(configuration) => configuration.uuid === savedUuid,
|
||||
);
|
||||
expect(deletedConfiguration).toBeUndefined();
|
||||
expect(mockMessager.publish).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should throw an error if configuration does not exist', async () => {
|
||||
await expect(
|
||||
deleteConfigurationUseCase.execute(
|
||||
new DeleteConfigurationCommand('wrong uuid'),
|
||||
),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { FindAllConfigurationsUseCase } from '../../domain/usecases/find-all-configurations.usecase';
|
||||
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
|
||||
|
||||
const findAllConfigurationsRequest: FindAllConfigurationsRequest =
|
||||
new FindAllConfigurationsRequest();
|
||||
findAllConfigurationsRequest.page = 1;
|
||||
findAllConfigurationsRequest.perPage = 10;
|
||||
|
||||
const findAllConfigurationsQuery: FindAllConfigurationsQuery =
|
||||
new FindAllConfigurationsQuery(findAllConfigurationsRequest);
|
||||
|
||||
const mockConfigurations: ICollection<Configuration> = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
||||
domain: Domain.USER,
|
||||
key: 'key2',
|
||||
value: 'value2',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
||||
domain: Domain.USER,
|
||||
key: 'key3',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findAll: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementation((query?: FindAllConfigurationsQuery) => {
|
||||
return Promise.resolve(mockConfigurations);
|
||||
}),
|
||||
};
|
||||
|
||||
describe('FindAllConfigurationsUseCase', () => {
|
||||
let findAllConfigurationsUseCase: FindAllConfigurationsUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
FindAllConfigurationsUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAllConfigurationsUseCase = module.get<FindAllConfigurationsUseCase>(
|
||||
FindAllConfigurationsUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAllConfigurationsUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return an array filled with configurations', async () => {
|
||||
const configurations = await findAllConfigurationsUseCase.execute(
|
||||
findAllConfigurationsQuery,
|
||||
);
|
||||
|
||||
expect(configurations).toBe(mockConfigurations);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
import { NotFoundException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { FindConfigurationByUuidRequest } from '../../domain/dtos/find-configuration-by-uuid.request';
|
||||
import { FindConfigurationByUuidUseCase } from '../../domain/usecases/find-configuration-by-uuid.usecase';
|
||||
import { FindConfigurationByUuidQuery } from '../../queries/find-configuration-by-uuid.query';
|
||||
|
||||
const mockConfiguration = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findOneByUuid: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((query?: FindConfigurationByUuidQuery) => {
|
||||
return Promise.resolve(mockConfiguration);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
return Promise.resolve(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('FindConfigurationByUuidUseCase', () => {
|
||||
let findConfigurationByUuidUseCase: FindConfigurationByUuidUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
FindConfigurationByUuidUseCase,
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findConfigurationByUuidUseCase = module.get<FindConfigurationByUuidUseCase>(
|
||||
FindConfigurationByUuidUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findConfigurationByUuidUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return a Configuration', async () => {
|
||||
const findConfigurationByUuidRequest: FindConfigurationByUuidRequest =
|
||||
new FindConfigurationByUuidRequest();
|
||||
findConfigurationByUuidRequest.uuid =
|
||||
'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
const Configuration = await findConfigurationByUuidUseCase.execute(
|
||||
new FindConfigurationByUuidQuery(findConfigurationByUuidRequest),
|
||||
);
|
||||
expect(Configuration).toBe(mockConfiguration);
|
||||
});
|
||||
it('should throw an error if Configuration does not exist', async () => {
|
||||
const findConfigurationByUuidRequest: FindConfigurationByUuidRequest =
|
||||
new FindConfigurationByUuidRequest();
|
||||
findConfigurationByUuidRequest.uuid =
|
||||
'bb281075-1b98-4456-89d6-c643d3044a90';
|
||||
await expect(
|
||||
findConfigurationByUuidUseCase.execute(
|
||||
new FindConfigurationByUuidQuery(findConfigurationByUuidRequest),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
|
||||
import { PrismaService } from '@modules/configuration/infrastructure/prisma.service';
|
||||
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Configuration repository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let configurationMapper: ConfigurationMapper;
|
||||
let eventEmitter: EventEmitter2;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [EventEmitterModule.forRoot()],
|
||||
providers: [PrismaService, ConfigurationMapper],
|
||||
}).compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
configurationMapper = module.get<ConfigurationMapper>(ConfigurationMapper);
|
||||
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||
});
|
||||
it('should be defined', () => {
|
||||
expect(
|
||||
new ConfigurationRepository(
|
||||
prismaService,
|
||||
configurationMapper,
|
||||
eventEmitter,
|
||||
mockMessagePublisher,
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { DeleteConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/delete-configuration.grpc.controller';
|
||||
import { DeleteConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const deleteConfigurationRequest: DeleteConfigurationRequestDto = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new DatabaseErrorException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Delete Configuration Grpc Controller', () => {
|
||||
let deleteConfigurationGrpcController: DeleteConfigurationGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
DeleteConfigurationGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteConfigurationGrpcController =
|
||||
module.get<DeleteConfigurationGrpcController>(
|
||||
DeleteConfigurationGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteConfigurationGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should delete a configuration item', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await deleteConfigurationGrpcController.delete(deleteConfigurationRequest);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if configuration item does not exist', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteConfigurationGrpcController.delete(
|
||||
deleteConfigurationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if a database error occurs', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteConfigurationGrpcController.delete(
|
||||
deleteConfigurationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.INTERNAL);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteConfigurationGrpcController.delete(
|
||||
deleteConfigurationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockConfigurationMapper = {
|
||||
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
})),
|
||||
};
|
||||
|
||||
describe('Get Configuration Grpc Controller', () => {
|
||||
let getConfigurationGrpcController: GetConfigurationGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: QueryBus,
|
||||
useValue: mockQueryBus,
|
||||
},
|
||||
{
|
||||
provide: ConfigurationMapper,
|
||||
useValue: mockConfigurationMapper,
|
||||
},
|
||||
GetConfigurationGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
getConfigurationGrpcController = module.get<GetConfigurationGrpcController>(
|
||||
GetConfigurationGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(getConfigurationGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return a configuration item', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockConfigurationMapper, 'toResponse');
|
||||
const response = await getConfigurationGrpcController.get({
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
});
|
||||
expect(response.value).toBe('3');
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfigurationMapper.toResponse).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if configuration is not found', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockConfigurationMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await getConfigurationGrpcController.get({
|
||||
domain: Domain.AD,
|
||||
key: 'price',
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfigurationMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockConfigurationMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await getConfigurationGrpcController.get({
|
||||
domain: Domain.AD,
|
||||
key: 'someValue',
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfigurationMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { PropagateConfigurationsGrpcController } from '@modules/configuration/interface/grpc-controllers/propagate-configurations.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Propagate Configurations Grpc Controller', () => {
|
||||
let propagateConfigurationsGrpcController: PropagateConfigurationsGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
PropagateConfigurationsGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
propagateConfigurationsGrpcController =
|
||||
module.get<PropagateConfigurationsGrpcController>(
|
||||
PropagateConfigurationsGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(propagateConfigurationsGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should propagate configuration items', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await propagateConfigurationsGrpcController.propagate();
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
try {
|
||||
await propagateConfigurationsGrpcController.propagate();
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
|
||||
import { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const setConfigurationRequest: SetConfigurationRequestDto = {
|
||||
domain: Domain.AD,
|
||||
key: 'seatsProposed',
|
||||
value: '3',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Set Configuration Grpc Controller', () => {
|
||||
let setConfigurationGrpcController: SetConfigurationGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
SetConfigurationGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
setConfigurationGrpcController = module.get<SetConfigurationGrpcController>(
|
||||
SetConfigurationGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(setConfigurationGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set a configuration item', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await setConfigurationGrpcController.set(
|
||||
setConfigurationRequest,
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await setConfigurationGrpcController.set(setConfigurationRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).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('configuration.create.info', 'my-test');
|
||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,100 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { PropagateConfigurationsUseCase } from '../../domain/usecases/propagate-configurations.usecase';
|
||||
import { ConfigurationProfile } from '../../mappers/configuration.profile';
|
||||
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
|
||||
|
||||
const propagateConfigurationsQuery: PropagateConfigurationsQuery =
|
||||
new PropagateConfigurationsQuery();
|
||||
|
||||
const mockConfigurations: ICollection<Configuration> = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
domain: Domain.USER,
|
||||
key: 'key1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
||||
domain: Domain.USER,
|
||||
key: 'key2',
|
||||
value: 'value2',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
||||
domain: Domain.USER,
|
||||
key: 'key3',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
};
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
findAll: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((query?: PropagateConfigurationsQuery) => {
|
||||
return Promise.resolve(mockConfigurations);
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((query?: PropagateConfigurationsQuery) => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('PropagateConfigurationsUseCase', () => {
|
||||
let propagateConfigurationsUseCase: PropagateConfigurationsUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
PropagateConfigurationsUseCase,
|
||||
ConfigurationProfile,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
propagateConfigurationsUseCase = module.get<PropagateConfigurationsUseCase>(
|
||||
PropagateConfigurationsUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(propagateConfigurationsUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should propagate configurations', async () => {
|
||||
jest.spyOn(mockMessager, 'publish');
|
||||
await propagateConfigurationsUseCase.execute(
|
||||
propagateConfigurationsQuery,
|
||||
);
|
||||
expect(mockMessager.publish).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should throw an error if repository call fails', async () => {
|
||||
await expect(
|
||||
propagateConfigurationsUseCase.execute(propagateConfigurationsQuery),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
||||
import { UpdateConfigurationCommand } from '../../commands/update-configuration.command';
|
||||
import { Domain } from '../../domain/dtos/domain.enum';
|
||||
import { UpdateConfigurationRequest } from '../../domain/dtos/update-configuration.request';
|
||||
import { Configuration } from '../../domain/entities/configuration';
|
||||
import { UpdateConfigurationUseCase } from '../../domain/usecases/update-configuration.usecase';
|
||||
import { ConfigurationProfile } from '../../mappers/configuration.profile';
|
||||
|
||||
const originalConfiguration: Configuration = new Configuration();
|
||||
originalConfiguration.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
originalConfiguration.domain = Domain.USER;
|
||||
originalConfiguration.key = 'key1';
|
||||
originalConfiguration.value = 'value1';
|
||||
|
||||
const updateConfigurationRequest: UpdateConfigurationRequest = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
value: 'value2',
|
||||
};
|
||||
|
||||
const updateConfigurationCommand: UpdateConfigurationCommand =
|
||||
new UpdateConfigurationCommand(updateConfigurationRequest);
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
update: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((uuid: string, params: any) => {
|
||||
originalConfiguration.value = params.value;
|
||||
|
||||
return Promise.resolve(originalConfiguration);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('UpdateConfigurationUseCase', () => {
|
||||
let updateConfigurationUseCase: UpdateConfigurationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigurationRepository,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
UpdateConfigurationUseCase,
|
||||
ConfigurationProfile,
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateConfigurationUseCase = module.get<UpdateConfigurationUseCase>(
|
||||
UpdateConfigurationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateConfigurationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should update a configuration value', async () => {
|
||||
jest.spyOn(mockMessager, 'publish');
|
||||
const updatedConfiguration: Configuration =
|
||||
await updateConfigurationUseCase.execute(updateConfigurationCommand);
|
||||
|
||||
expect(updatedConfiguration.value).toBe(updateConfigurationRequest.value);
|
||||
expect(mockMessager.publish).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should throw an error if configuration does not exist', async () => {
|
||||
await expect(
|
||||
updateConfigurationUseCase.execute(updateConfigurationCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,259 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { DatabaseException } from '../../exceptions/database.exception';
|
||||
import { ICollection } from '../../interfaces/collection.interface';
|
||||
import { IRepository } from '../../interfaces/repository.interface';
|
||||
import { PrismaService } from './prisma-service';
|
||||
|
||||
/**
|
||||
* Child classes MUST redefined _model property with appropriate model name
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class PrismaRepository<T> implements IRepository<T> {
|
||||
protected model: string;
|
||||
|
||||
constructor(protected readonly prisma: PrismaService) {}
|
||||
|
||||
findAll = async (
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
where?: any,
|
||||
include?: any,
|
||||
): Promise<ICollection<T>> => {
|
||||
const [data, total] = await this.prisma.$transaction([
|
||||
this.prisma[this.model].findMany({
|
||||
where,
|
||||
include,
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage,
|
||||
}),
|
||||
this.prisma[this.model].count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
return Promise.resolve({
|
||||
data,
|
||||
total,
|
||||
});
|
||||
};
|
||||
|
||||
findOneByUuid = async (uuid: string): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].findUnique({
|
||||
where: { uuid },
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findOne = async (where: any, include?: any): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].findFirst({
|
||||
where: where,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO : using any is not good, but needed for nested entities
|
||||
// TODO : Refactor for good clean architecture ?
|
||||
create = async (entity: Partial<T> | any, include?: any): Promise<T> => {
|
||||
try {
|
||||
const res = await this.prisma[this.model].create({
|
||||
data: entity,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
update = async (uuid: string, entity: Partial<T>): Promise<T> => {
|
||||
try {
|
||||
const updatedEntity = await this.prisma[this.model].update({
|
||||
where: { uuid },
|
||||
data: entity,
|
||||
});
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWhere = async (
|
||||
where: any,
|
||||
entity: Partial<T> | any,
|
||||
include?: any,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
const updatedEntity = await this.prisma[this.model].update({
|
||||
where: where,
|
||||
data: entity,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (uuid: string): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].delete({
|
||||
where: { uuid },
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
deleteMany = async (where: any): Promise<void> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].deleteMany({
|
||||
where: where,
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findAllByQuery = async (
|
||||
include: string[],
|
||||
where: string[],
|
||||
): Promise<ICollection<T>> => {
|
||||
const query = `SELECT ${include.join(',')} FROM ${
|
||||
this.model
|
||||
} WHERE ${where.join(' AND ')}`;
|
||||
const data: T[] = await this.prisma.$queryRawUnsafe(query);
|
||||
return Promise.resolve({
|
||||
data,
|
||||
total: data.length,
|
||||
});
|
||||
};
|
||||
|
||||
createWithFields = async (fields: object): Promise<number> => {
|
||||
try {
|
||||
const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
|
||||
'","',
|
||||
)}") VALUES (${Object.values(fields).join(',')})`;
|
||||
return await this.prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWithFields = async (uuid: string, entity: object): Promise<number> => {
|
||||
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
|
||||
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
|
||||
try {
|
||||
const command = `UPDATE ${this.model} SET ${values.join(
|
||||
', ',
|
||||
)} WHERE uuid = '${uuid}'`;
|
||||
return await this.prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
healthCheck = async (): Promise<boolean> => {
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
this.$on('beforeExit', async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from './adapters/secondaries/prisma-service';
|
||||
import { ConfigRepository } from './domain/configuration.repository';
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService, ConfigRepository],
|
||||
exports: [PrismaService, ConfigRepository],
|
||||
})
|
||||
export class DatabaseModule {}
|
|
@ -1,3 +0,0 @@
|
|||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
||||
|
||||
export class ConfigRepository<T> extends PrismaRepository<T> {}
|
|
@ -1,24 +0,0 @@
|
|||
export class DatabaseException implements Error {
|
||||
name: string;
|
||||
message: string;
|
||||
|
||||
constructor(
|
||||
private _type: string = 'unknown',
|
||||
private _code: string = '',
|
||||
message?: string,
|
||||
) {
|
||||
this.name = 'DatabaseException';
|
||||
this.message = message ?? 'An error occured with the database.';
|
||||
if (this.message.includes('Unique constraint failed')) {
|
||||
this.message = 'Already exists.';
|
||||
}
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
get code(): string {
|
||||
return this._code;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export interface ICollection<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { ICollection } from './collection.interface';
|
||||
|
||||
export interface IRepository<T> {
|
||||
findAll(
|
||||
page: number,
|
||||
perPage: number,
|
||||
params?: any,
|
||||
include?: any,
|
||||
): Promise<ICollection<T>>;
|
||||
findOne(where: any, include?: any): Promise<T>;
|
||||
findOneByUuid(uuid: string, include?: any): Promise<T>;
|
||||
create(entity: Partial<T> | any, include?: any): Promise<T>;
|
||||
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
|
||||
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
|
||||
delete(uuid: string): Promise<T>;
|
||||
deleteMany(where: any): Promise<void>;
|
||||
}
|
|
@ -1,571 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaService } from '../../adapters/secondaries/prisma-service';
|
||||
import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
|
||||
import { DatabaseException } from '../../exceptions/database.exception';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
class FakeEntity {
|
||||
uuid?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
let entityId = 2;
|
||||
const entityUuid = 'uuid-';
|
||||
const entityName = 'name-';
|
||||
|
||||
const createRandomEntity = (): FakeEntity => {
|
||||
const entity: FakeEntity = {
|
||||
uuid: `${entityUuid}${entityId}`,
|
||||
name: `${entityName}${entityId}`,
|
||||
};
|
||||
|
||||
entityId++;
|
||||
|
||||
return entity;
|
||||
};
|
||||
|
||||
const fakeEntityToCreate: FakeEntity = {
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const fakeEntityCreated: FakeEntity = {
|
||||
...fakeEntityToCreate,
|
||||
uuid: 'some-uuid',
|
||||
};
|
||||
|
||||
const fakeEntities: FakeEntity[] = [];
|
||||
Array.from({ length: 10 }).forEach(() => {
|
||||
fakeEntities.push(createRandomEntity());
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
|
||||
protected model = 'fake';
|
||||
}
|
||||
|
||||
class FakePrismaService extends PrismaService {
|
||||
fake: any;
|
||||
}
|
||||
|
||||
const mockPrismaService = {
|
||||
$transaction: jest.fn().mockImplementation(async (data: any) => {
|
||||
const entities = await data[0];
|
||||
if (entities.length == 1) {
|
||||
return Promise.resolve([[fakeEntityCreated], 1]);
|
||||
}
|
||||
|
||||
return Promise.resolve([fakeEntities, fakeEntities.length]);
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
$queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
|
||||
return Promise.resolve(fakeEntities);
|
||||
}),
|
||||
$executeRawUnsafe: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Error('an unknown error');
|
||||
})
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
$queryRaw: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return true;
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
}),
|
||||
fake: {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
|
||||
findMany: jest.fn().mockImplementation((params?: any) => {
|
||||
if (params?.where?.limit == 1) {
|
||||
return Promise.resolve([fakeEntityCreated]);
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeEntities);
|
||||
}),
|
||||
count: jest.fn().mockResolvedValue(fakeEntities.length),
|
||||
|
||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
||||
let entity;
|
||||
|
||||
if (params?.where?.uuid) {
|
||||
entity = fakeEntities.find(
|
||||
(entity) => entity.uuid === params?.where?.uuid,
|
||||
);
|
||||
}
|
||||
|
||||
if (!entity && params?.where?.uuid == 'unknown') {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
} else if (!entity) {
|
||||
throw new Error('no entity');
|
||||
}
|
||||
|
||||
return entity;
|
||||
}),
|
||||
|
||||
findFirst: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
if (params?.where?.name) {
|
||||
return Promise.resolve(
|
||||
fakeEntities.find((entity) => entity.name === params?.where?.name),
|
||||
);
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
|
||||
update: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce((params: any) => {
|
||||
const entity = fakeEntities.find(
|
||||
(entity) => entity.name === params.where.name,
|
||||
);
|
||||
Object.entries(params.data).map(([key, value]) => {
|
||||
entity[key] = value;
|
||||
});
|
||||
|
||||
return Promise.resolve(entity);
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
const entity = fakeEntities.find(
|
||||
(entity) => entity.uuid === params.where.uuid,
|
||||
);
|
||||
Object.entries(params.data).map(([key, value]) => {
|
||||
entity[key] = value;
|
||||
});
|
||||
|
||||
return Promise.resolve(entity);
|
||||
}),
|
||||
|
||||
delete: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
let found = false;
|
||||
|
||||
fakeEntities.forEach((entity, index) => {
|
||||
if (entity.uuid === params?.where?.uuid) {
|
||||
found = true;
|
||||
fakeEntities.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
|
||||
deleteMany: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
let found = false;
|
||||
|
||||
fakeEntities.forEach((entity, index) => {
|
||||
if (entity.uuid === params?.where?.uuid) {
|
||||
found = true;
|
||||
fakeEntities.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe('PrismaRepository', () => {
|
||||
let fakeRepository: FakePrismaRepository;
|
||||
let prisma: FakePrismaService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FakePrismaRepository,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
|
||||
prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(fakeRepository).toBeDefined();
|
||||
expect(prisma).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an array of entities', async () => {
|
||||
jest.spyOn(prisma.fake, 'findMany');
|
||||
jest.spyOn(prisma.fake, 'count');
|
||||
jest.spyOn(prisma, '$transaction');
|
||||
|
||||
const entities = await fakeRepository.findAll();
|
||||
expect(entities).toStrictEqual({
|
||||
data: fakeEntities,
|
||||
total: fakeEntities.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array containing only one entity', async () => {
|
||||
const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
|
||||
|
||||
expect(prisma.fake.findMany).toHaveBeenCalledWith({
|
||||
skip: 0,
|
||||
take: 10,
|
||||
where: { limit: 1 },
|
||||
});
|
||||
expect(entities).toEqual({
|
||||
data: [fakeEntityCreated],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create an entity', async () => {
|
||||
jest.spyOn(prisma.fake, 'create');
|
||||
|
||||
const newEntity = await fakeRepository.create(fakeEntityToCreate);
|
||||
expect(newEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.create(fakeEntityToCreate),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.create(fakeEntityToCreate),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneByUuid', () => {
|
||||
it('should find an entity by uuid', async () => {
|
||||
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
|
||||
expect(entity).toBe(fakeEntities[0]);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOneByUuid('unknown'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOneByUuid('wrong-uuid'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find one entity', async () => {
|
||||
const entity = await fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
});
|
||||
|
||||
expect(entity.name).toBe(fakeEntities[0].name);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for unknown error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
await expect(
|
||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should update an entity with name', async () => {
|
||||
const newName = 'new-random-name';
|
||||
|
||||
await fakeRepository.updateWhere(
|
||||
{ name: fakeEntities[0].name },
|
||||
{
|
||||
name: newName,
|
||||
},
|
||||
);
|
||||
expect(fakeEntities[0].name).toBe(newName);
|
||||
});
|
||||
|
||||
it('should update an entity with uuid', async () => {
|
||||
const newName = 'random-name';
|
||||
|
||||
await fakeRepository.update(fakeEntities[0].uuid, {
|
||||
name: newName,
|
||||
});
|
||||
expect(fakeEntities[0].name).toBe(newName);
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(
|
||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
await expect(
|
||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an entity', async () => {
|
||||
const savedUuid = fakeEntities[0].uuid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const res = await fakeRepository.delete(savedUuid);
|
||||
|
||||
const deletedEntity = fakeEntities.find(
|
||||
(entity) => entity.uuid === savedUuid,
|
||||
);
|
||||
expect(deletedEntity).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should delete entities based on their uuid', async () => {
|
||||
const savedUuid = fakeEntities[0].uuid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const res = await fakeRepository.deleteMany({ uuid: savedUuid });
|
||||
|
||||
const deletedEntity = fakeEntities.find(
|
||||
(entity) => entity.uuid === savedUuid,
|
||||
);
|
||||
expect(deletedEntity).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(
|
||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAllByquery', () => {
|
||||
it('should return an array of entities', async () => {
|
||||
const entities = await fakeRepository.findAllByQuery(
|
||||
['uuid', 'name'],
|
||||
['name is not null'],
|
||||
);
|
||||
expect(entities).toStrictEqual({
|
||||
data: fakeEntities,
|
||||
total: fakeEntities.length,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWithFields', () => {
|
||||
it('should create an entity', async () => {
|
||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
||||
|
||||
const newEntity = await fakeRepository.createWithFields({
|
||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
name: 'my-name',
|
||||
});
|
||||
expect(newEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.createWithFields({
|
||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
name: 'my-name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.createWithFields({
|
||||
name: 'my-name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateWithFields', () => {
|
||||
it('should update an entity', async () => {
|
||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
||||
|
||||
const updatedEntity = await fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
);
|
||||
expect(updatedEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('healthCheck', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a healthy result', async () => {
|
||||
const res = await fakeRepository.healthCheck();
|
||||
expect(res).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should throw an exception if database is not available', async () => {
|
||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0,
|
||||
SERVING = 1,
|
||||
NOT_SERVING = 2,
|
||||
}
|
||||
|
||||
interface HealthCheckRequest {
|
||||
service: string;
|
||||
}
|
||||
|
||||
interface HealthCheckResponse {
|
||||
status: ServingStatus;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class HealthServerController {
|
||||
constructor(
|
||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('Health', 'Check')
|
||||
async check(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
data: HealthCheckRequest,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
metadata: any,
|
||||
): Promise<HealthCheckResponse> {
|
||||
const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy(
|
||||
'prisma',
|
||||
);
|
||||
return {
|
||||
status:
|
||||
healthCheck['prisma'].status == 'up'
|
||||
? ServingStatus.SERVING
|
||||
: ServingStatus.NOT_SERVING,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckService,
|
||||
HealthCheck,
|
||||
HealthCheckResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { Messager } from '../secondaries/messager';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
private readonly healthCheckService: HealthCheckService,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
async check() {
|
||||
try {
|
||||
return await this.healthCheckService.check([
|
||||
async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
||||
]);
|
||||
} catch (error) {
|
||||
const healthCheckResult: HealthCheckResult = error.response;
|
||||
this.messager.publish(
|
||||
'logging.configuration.health.crit',
|
||||
JSON.stringify(healthCheckResult.error),
|
||||
);
|
||||
throw 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;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckError,
|
||||
HealthIndicator,
|
||||
HealthIndicatorResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { ConfigurationRepository } from '../../../configuration/adapters/secondaries/configuration.repository';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
|
||||
constructor(private readonly repository: ConfigurationRepository) {
|
||||
super();
|
||||
}
|
||||
|
||||
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
|
||||
try {
|
||||
await this.repository.healthCheck();
|
||||
return this.getStatus(key, true);
|
||||
} catch (e) {
|
||||
throw new HealthCheckError('Prisma', {
|
||||
prisma: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue