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
|
# PRISMA
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration"
|
||||||
|
|
||||||
# RABBIT MQ
|
# MESSAGE BROKER
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||||
RMQ_EXCHANGE=mobicoop
|
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.
|
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 :
|
Each item consists in :
|
||||||
|
|
||||||
- a **uuid** : a unique identifier for the configuration item
|
- 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
|
## Available domains
|
||||||
|
|
||||||
- **USER** : user related configuration item
|
- **AD** : ad related configuration items
|
||||||
|
- **MATCHER** : matching algotithm related configuration items
|
||||||
|
- **USER** : user related configuration items
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
@ -63,24 +67,16 @@ npm run migrate
|
||||||
|
|
||||||
The app exposes the following [gRPC](https://grpc.io/) services :
|
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
|
```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_)
|
- **Set** : create or update a configuration item
|
||||||
|
|
||||||
```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)
|
|
||||||
|
|
||||||
```json
|
```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
|
```json
|
||||||
{
|
{
|
||||||
"value": "value2",
|
"domain": "AD",
|
||||||
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae"
|
"key": "seatsProposed"
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Delete** : delete a configuration item by its uuid
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -117,9 +105,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
As mentionned earlier, RabbitMQ messages are sent after these events :
|
As mentionned earlier, RabbitMQ messages are sent after these events :
|
||||||
|
|
||||||
- **Create** (message : the created configuration item informations)
|
- **Set** (message : the created / updated configuration item informations)
|
||||||
|
|
||||||
- **Update** (message : the updated configuration item informations)
|
|
||||||
|
|
||||||
- **Delete** (message : the uuid of the deleted configuration item)
|
- **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",
|
"name": "@mobicoop/configuration",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "Mobicoop V3 Configuration Service",
|
"description": "Mobicoop V3 Configuration Service",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -31,50 +31,52 @@
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automapper/classes": "^8.7.7",
|
"@grpc/grpc-js": "^1.9.6",
|
||||||
"@automapper/core": "^8.7.7",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@automapper/nestjs": "^8.7.7",
|
"@mobicoop/ddd-library": "^2.1.1",
|
||||||
"@golevelup/nestjs-rabbitmq": "^3.4.0",
|
"@mobicoop/health-module": "^2.3.1",
|
||||||
"@grpc/grpc-js": "^1.8.5",
|
"@mobicoop/message-broker-module": "^2.1.1",
|
||||||
"@grpc/proto-loader": "^0.7.4",
|
"@nestjs/common": "^10.2.7",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/core": "^10.2.7",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/cqrs": "^10.2.6",
|
||||||
"@nestjs/cqrs": "^9.0.1",
|
"@nestjs/event-emitter": "^2.0.2",
|
||||||
"@nestjs/microservices": "^9.2.1",
|
"@nestjs/microservices": "^10.2.7",
|
||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^10.2.7",
|
||||||
"@nestjs/terminus": "^9.2.2",
|
"@nestjs/terminus": "^10.1.1",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^5.4.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^5.0.5",
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.8.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^10.1.18",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^10.0.2",
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^10.2.7",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.20",
|
||||||
"@types/jest": "28.1.8",
|
"@types/jest": "29.5.6",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^20.8.7",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.15",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@types/uuid": "^9.0.6",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||||
"dotenv-cli": "^7.0.0",
|
"@typescript-eslint/parser": "^6.8.0",
|
||||||
"eslint": "^8.0.1",
|
"dotenv-cli": "^7.3.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint": "^8.51.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"jest": "28.1.3",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"prettier": "^2.3.2",
|
"jest": "29.7.0",
|
||||||
"prisma": "^4.9.0",
|
"prettier": "^3.0.3",
|
||||||
"source-map-support": "^0.5.20",
|
"prisma": "^5.4.2",
|
||||||
"supertest": "^6.1.3",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-jest": "28.0.8",
|
"supertest": "^6.3.3",
|
||||||
"ts-loader": "^9.2.3",
|
"ts-jest": "29.1.1",
|
||||||
"ts-node": "^10.0.0",
|
"ts-loader": "^9.5.0",
|
||||||
"tsconfig-paths": "4.1.0",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.7.4"
|
"tsconfig-paths": "4.2.0",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
@ -83,12 +85,13 @@
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
".controller.ts",
|
".constants.ts",
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
|
"prisma.service.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
@ -100,15 +103,20 @@
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
".controller.ts",
|
".constants.ts",
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
|
"prisma.service.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||||
|
"^@src(.*)": "<rootDir>$1"
|
||||||
|
},
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
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 {
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
HealthModule,
|
||||||
|
HealthModuleOptions,
|
||||||
|
HealthRepositoryPort,
|
||||||
|
} from '@mobicoop/health-module';
|
||||||
|
import { MessagerModule } from '@modules/messager/messager.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ConfigurationModule } from './modules/configuration/configuration.module';
|
import {
|
||||||
import { HealthModule } from './modules/health/health.module';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
EventEmitterModule.forRoot(),
|
||||||
ConfigurationModule,
|
HealthModule.forRootAsync({
|
||||||
HealthModule,
|
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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
controllers: [],
|
messagePublisher,
|
||||||
providers: [],
|
}),
|
||||||
|
}),
|
||||||
|
ConfigurationModule,
|
||||||
|
MessagerModule,
|
||||||
|
],
|
||||||
|
exports: [ConfigurationModule, MessagerModule],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
||||||
|
|
||||||
package health;
|
package health;
|
||||||
|
|
||||||
|
|
||||||
service Health {
|
service Health {
|
||||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
}
|
}
|
||||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
||||||
NOT_SERVING = 2;
|
NOT_SERVING = 2;
|
||||||
}
|
}
|
||||||
ServingStatus status = 1;
|
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 { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { GRPC_HEALTH_PACKAGE_NAME, GRPC_PACKAGE_NAME } from './app.constants';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
@ -11,20 +12,20 @@ async function bootstrap() {
|
||||||
app.connectMicroservice<MicroserviceOptions>({
|
app.connectMicroservice<MicroserviceOptions>({
|
||||||
transport: Transport.GRPC,
|
transport: Transport.GRPC,
|
||||||
options: {
|
options: {
|
||||||
package: ['configuration', 'health'],
|
package: [GRPC_PACKAGE_NAME, GRPC_HEALTH_PACKAGE_NAME],
|
||||||
protoPath: [
|
protoPath: [
|
||||||
join(
|
join(
|
||||||
__dirname,
|
__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,
|
url: `${process.env.SERVICE_URL}:${process.env.SERVICE_PORT}`,
|
||||||
loader: { keepCase: true },
|
loader: { keepCase: true, enums: String },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.startAllMicroservices();
|
await app.startAllMicroservices();
|
||||||
await app.listen(process.env.HEALTH_SERVICE_PORT);
|
await app.listen(process.env.HEALTH_SERVICE_PORT as unknown as number);
|
||||||
}
|
}
|
||||||
bootstrap();
|
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, Provider } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller';
|
||||||
import { ConfigurationController } from './adapters/primaries/configuration.controller';
|
import { SetConfigurationGrpcController } from './interface/grpc-controllers/set-configuration.grpc.controller';
|
||||||
import { Messager } from './adapters/secondaries/messager';
|
import { DeleteConfigurationGrpcController } from './interface/grpc-controllers/delete-configuration.grpc.controller';
|
||||||
import { ConfigurationRepository } from './adapters/secondaries/configuration.repository';
|
import { PropagateConfigurationsGrpcController } from './interface/grpc-controllers/propagate-configurations.grpc.controller';
|
||||||
import { CreateConfigurationUseCase } from './domain/usecases/create-configuration.usecase';
|
import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler';
|
||||||
import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase';
|
import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler';
|
||||||
import { FindAllConfigurationsUseCase } from './domain/usecases/find-all-configurations.usecase';
|
import { SetConfigurationService } from './core/application/commands/set-configuration/set-configuration.service';
|
||||||
import { FindConfigurationByUuidUseCase } from './domain/usecases/find-configuration-by-uuid.usecase';
|
import { DeleteConfigurationService } from './core/application/commands/delete-configuration/delete-configuration.service';
|
||||||
import { PropagateConfigurationsUseCase } from './domain/usecases/propagate-configurations.usecase';
|
import { GetConfigurationQueryHandler } from './core/application/queries/get-configuration/get-configuration.query-handler';
|
||||||
import { UpdateConfigurationUseCase } from './domain/usecases/update-configuration.usecase';
|
import { ConfigurationMapper } from './configuration.mapper';
|
||||||
import { ConfigurationProfile } from './mappers/configuration.profile';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [CqrsModule],
|
||||||
DatabaseModule,
|
controllers: [...grpcControllers],
|
||||||
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],
|
|
||||||
providers: [
|
providers: [
|
||||||
ConfigurationProfile,
|
...eventHandlers,
|
||||||
ConfigurationRepository,
|
...commandHandlers,
|
||||||
Messager,
|
...queryHandlers,
|
||||||
FindAllConfigurationsUseCase,
|
...mappers,
|
||||||
FindConfigurationByUuidUseCase,
|
...repositories,
|
||||||
CreateConfigurationUseCase,
|
...messagePublishers,
|
||||||
UpdateConfigurationUseCase,
|
...orms,
|
||||||
DeleteConfigurationUseCase,
|
|
||||||
PropagateConfigurationsUseCase,
|
|
||||||
],
|
],
|
||||||
|
exports: [PrismaService, ConfigurationMapper, CONFIGURATION_REPOSITORY],
|
||||||
})
|
})
|
||||||
export class ConfigurationModule {}
|
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 {
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
CONFIGURATION_MESSAGE_PUBLISHER,
|
||||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
CONFIGURATION_REPOSITORY,
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
} from '@modules/configuration/configuration.di-tokens';
|
||||||
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
|
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||||
import { Domain } from '../../domain/dtos/domain.enum';
|
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||||
import { Configuration } from '../../domain/entities/configuration';
|
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 prismaService: PrismaService;
|
||||||
let configurationRepository: ConfigurationRepository;
|
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) => {
|
const createConfigurations = async (nbToCreate = 10) => {
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
for (let i = 0; i < nbToCreate; i++) {
|
||||||
await prismaService.configuration.create({
|
const configurationToCreate = {
|
||||||
data: {
|
uuid: getSeed(i, baseUuid.uuid),
|
||||||
domain: Domain.USER,
|
domain: Domain.AD,
|
||||||
key: `key-${i}`,
|
key: `key${i}`,
|
||||||
value: `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 () => {
|
beforeAll(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module = await Test.createTestingModule({
|
||||||
imports: [DatabaseModule],
|
imports: [
|
||||||
providers: [ConfigurationRepository, PrismaService],
|
EventEmitterModule.forRoot(),
|
||||||
}).compile();
|
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);
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
configurationRepository = module.get<ConfigurationRepository>(
|
configurationRepository = module.get<ConfigurationRepository>(
|
||||||
ConfigurationRepository,
|
CONFIGURATION_REPOSITORY,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,78 +94,23 @@ describe('ConfigurationRepository', () => {
|
||||||
await prismaService.configuration.deleteMany();
|
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', () => {
|
describe('findOne', () => {
|
||||||
it('should return a configuration according to its domain and key', async () => {
|
it('should return a configuration', async () => {
|
||||||
const configurationToFind = await prismaService.configuration.create({
|
await createConfigurations(1);
|
||||||
data: {
|
const result = await configurationRepository.findOne({
|
||||||
domain: Domain.USER,
|
domain: Domain.AD,
|
||||||
key: 'key1',
|
key: 'key0',
|
||||||
value: 'value1',
|
});
|
||||||
},
|
expect(result.getProps().value).toBe('value0');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const configuration = await configurationRepository.findOne({
|
describe('findAll', () => {
|
||||||
domain: Domain.USER,
|
it('should return all configurations', async () => {
|
||||||
key: 'key1',
|
await createConfigurations(10);
|
||||||
});
|
const configurations: ConfigurationEntity[] =
|
||||||
|
await configurationRepository.findAll({});
|
||||||
expect(configuration.uuid).toBe(configurationToFind.uuid);
|
expect(configurations).toHaveLength(10);
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null with unknown domain and key', async () => {
|
|
||||||
const configuration = await configurationRepository.findOne({
|
|
||||||
domain: Domain.USER,
|
|
||||||
key: 'key1',
|
|
||||||
});
|
|
||||||
expect(configuration).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,75 +118,57 @@ describe('ConfigurationRepository', () => {
|
||||||
it('should create a configuration', async () => {
|
it('should create a configuration', async () => {
|
||||||
const beforeCount = await prismaService.configuration.count();
|
const beforeCount = await prismaService.configuration.count();
|
||||||
|
|
||||||
const configurationToCreate: Configuration = new Configuration();
|
const createConfigurationProps: CreateConfigurationProps = {
|
||||||
configurationToCreate.domain = Domain.USER;
|
domain: Domain.AD,
|
||||||
configurationToCreate.key = 'key1';
|
key: 'seatsProposed',
|
||||||
configurationToCreate.value = 'value1';
|
value: '3',
|
||||||
const configuration = await configurationRepository.create(
|
};
|
||||||
configurationToCreate,
|
|
||||||
);
|
const configurationToCreate: ConfigurationEntity =
|
||||||
|
ConfigurationEntity.create(createConfigurationProps);
|
||||||
|
await configurationRepository.insert(configurationToCreate);
|
||||||
|
|
||||||
const afterCount = await prismaService.configuration.count();
|
const afterCount = await prismaService.configuration.count();
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
expect(afterCount - beforeCount).toBe(1);
|
||||||
expect(configuration.uuid).toBeDefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('should update configuration value', async () => {
|
it('should update a configuration', async () => {
|
||||||
const configurationToUpdate = await prismaService.configuration.create({
|
await createConfigurations(1);
|
||||||
data: {
|
const configurationToUpdate: ConfigurationEntity =
|
||||||
domain: Domain.USER,
|
await configurationRepository.findOne({
|
||||||
key: 'key1',
|
domain: Domain.AD,
|
||||||
value: 'value1',
|
key: 'key0',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
configurationToUpdate.update({ value: 'newValue' });
|
||||||
const toUpdate: Configuration = new Configuration();
|
await configurationRepository.update(
|
||||||
toUpdate.value = 'value2';
|
configurationToUpdate.id,
|
||||||
const updatedConfiguration = await configurationRepository.update(
|
configurationToUpdate,
|
||||||
configurationToUpdate.uuid,
|
|
||||||
toUpdate,
|
|
||||||
);
|
);
|
||||||
|
const result: ConfigurationEntity = await configurationRepository.findOne(
|
||||||
expect(updatedConfiguration.uuid).toBe(configurationToUpdate.uuid);
|
{
|
||||||
expect(updatedConfiguration.value).toBe('value2');
|
domain: Domain.AD,
|
||||||
});
|
key: 'key0',
|
||||||
|
},
|
||||||
it('should throw DatabaseException', async () => {
|
);
|
||||||
const toUpdate: Configuration = new Configuration();
|
expect(result.getProps().value).toBe('newValue');
|
||||||
toUpdate.key = 'updated';
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
configurationRepository.update(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
toUpdate,
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
it('should delete a configuration', async () => {
|
it('should delete a configuration', async () => {
|
||||||
const configurationToRemove = await prismaService.configuration.create({
|
await createConfigurations(10);
|
||||||
data: {
|
const beforeCount = await prismaService.configuration.count();
|
||||||
domain: Domain.USER,
|
const configurationToDelete: ConfigurationEntity =
|
||||||
key: 'key1',
|
await configurationRepository.findOne({
|
||||||
value: 'value1',
|
domain: Domain.AD,
|
||||||
},
|
key: 'key4',
|
||||||
});
|
});
|
||||||
|
await configurationRepository.delete(configurationToDelete);
|
||||||
await configurationRepository.delete(configurationToRemove.uuid);
|
const afterCount = await prismaService.configuration.count();
|
||||||
|
expect(afterCount - beforeCount).toBe(-1);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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