Merge branch 'useLibrary' into 'main'

Use configuration package

See merge request v3/service/configuration!26
This commit is contained in:
Sylvain Briat 2023-10-25 14:06:00 +00:00
commit 09f0142325
28 changed files with 561 additions and 750 deletions

View File

@ -1,6 +1,6 @@
# Mobicoop V3 - Configuration Service # Mobicoop V3 - Configuration Service
Configuration items management. Used to configure and store all services using redis as database. Configuration items management. Used to configure and store configuration items for all Mobicoop V3 services using redis as database.
Each item consists in : Each item consists in :
@ -8,10 +8,6 @@ Each item consists in :
- a **key** : the key of the configuration item (a string) - a **key** : the key of the configuration item (a string)
- a **value** : the value of the configuration item (always a string, each service must cast the value if needed) - a **value** : the value of the configuration item (always a string, each service must cast the value if needed)
This service centralizes the configuration items, but theoratically each service should "push" its items toward the configuration service.
Practically, it's the other way round as it's easier to use this configuration service as the single source of truth. This is why configuration items key and domain are immutable : services may use hardcoded domain-key pairs. Therefore, only values can be updated.
## Available domains ## Available domains
- **CARPOOL** : carpool related configuration items (eg. default number of seats proposed as a driver) - **CARPOOL** : carpool related configuration items (eg. default number of seats proposed as a driver)
@ -27,8 +23,6 @@ You also need NodeJS installed locally : we **strongly** advise to install [Node
The API will run inside a docker container, **but** the install itself is made outside the container, because during development we need tools that need to be available locally (eg. ESLint, Prettier...). The API will run inside a docker container, **but** the install itself is made outside the container, because during development we need tools that need to be available locally (eg. ESLint, Prettier...).
A RabbitMQ instance is also required to send / receive messages when data has been inserted/updated/deleted.
## Installation ## Installation
- copy `.env.dist` to `.env` : - copy `.env.dist` to `.env` :

751
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@mobicoop/configuration", "name": "@mobicoop/configuration",
"version": "2.0.0", "version": "2.1.0",
"description": "Mobicoop V3 Configuration Service", "description": "Mobicoop V3 Configuration Service",
"author": "sbriat", "author": "sbriat",
"private": true, "private": true,
@ -26,6 +26,7 @@
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.6", "@grpc/grpc-js": "^1.9.6",
"@grpc/proto-loader": "^0.7.10", "@grpc/proto-loader": "^0.7.10",
"@mobicoop/configuration-module": "^4.0.0",
"@mobicoop/ddd-library": "^2.1.1", "@mobicoop/ddd-library": "^2.1.1",
"@mobicoop/health-module": "^2.3.1", "@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1", "@mobicoop/message-broker-module": "^2.1.1",
@ -37,14 +38,10 @@
"@nestjs/microservices": "^10.2.7", "@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7", "@nestjs/platform-express": "^10.2.7",
"@nestjs/terminus": "^10.1.1", "@nestjs/terminus": "^10.1.1",
"@songkeys/nestjs-redis": "^10.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^5.0.5", "rimraf": "^5.0.5"
"rxjs": "^7.8.1",
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.1.18", "@nestjs/cli": "^10.1.18",
@ -54,7 +51,6 @@
"@types/jest": "29.5.6", "@types/jest": "29.5.6",
"@types/node": "^20.8.7", "@types/node": "^20.8.7",
"@types/supertest": "^2.0.15", "@types/supertest": "^2.0.15",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0", "@typescript-eslint/parser": "^6.8.0",
"dotenv-cli": "^7.3.0", "dotenv-cli": "^7.3.0",

View File

@ -4,6 +4,10 @@ import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants'; import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
import { MessagePublisherPort } from '@mobicoop/ddd-library'; import { MessagePublisherPort } from '@mobicoop/ddd-library';
import {
ConfigurationModuleOptions,
ConfigurationModule as ConfigurationModulePackage,
} from '@mobicoop/configuration-module';
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens'; import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
import { ConfigurationModule } from '@modules/configuration/configuration.module'; import { ConfigurationModule } from '@modules/configuration/configuration.module';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
@ -11,7 +15,6 @@ import brokerConfig from './config/broker.config';
import carpoolConfig from './config/carpool.config'; import carpoolConfig from './config/carpool.config';
import paginationConfig from './config/pagination.config'; import paginationConfig from './config/pagination.config';
import serviceConfig from './config/service.config'; import serviceConfig from './config/service.config';
import { RedisModule, RedisModuleOptions } from '@songkeys/nestjs-redis';
import redisConfig from './config/redis.config'; import redisConfig from './config/redis.config';
import { Transport } from '@nestjs/microservices'; import { Transport } from '@nestjs/microservices';
@ -49,18 +52,16 @@ import { Transport } from '@nestjs/microservices';
], ],
}), }),
}), }),
RedisModule.forRootAsync({ ConfigurationModulePackage.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async ( useFactory: async (
configService: ConfigService, configService: ConfigService,
): Promise<RedisModuleOptions> => { ): Promise<ConfigurationModuleOptions> => {
return { return {
config: {
host: configService.get<string>('redis.host') as string, host: configService.get<string>('redis.host') as string,
port: configService.get<number>('redis.port') as number, port: configService.get<number>('redis.port') as number,
password: configService.get<string>('redis.password'), password: configService.get<string>('redis.password'),
},
}; };
}, },
}), }),

View File

@ -1,5 +1,13 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
export interface CarpoolConfig {
departureTimeMargin: number;
role: string;
seatsProposed: number;
seatsRequested: number;
strictFrequency: boolean;
}
export default registerAs('carpool', () => ({ export default registerAs('carpool', () => ({
departureTimeMargin: process.env.DEPARTURE_TIME_MARGIN departureTimeMargin: process.env.DEPARTURE_TIME_MARGIN
? parseInt(process.env.DEPARTURE_TIME_MARGIN, 10) ? parseInt(process.env.DEPARTURE_TIME_MARGIN, 10)

View File

@ -1,5 +1,9 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
export interface PaginationConfig {
perPage: number;
}
export default registerAs('pagination', () => ({ export default registerAs('pagination', () => ({
perPage: process.env.PER_PAGE ? parseInt(process.env.PER_PAGE, 10) : 10, perPage: process.env.PER_PAGE ? parseInt(process.env.PER_PAGE, 10) : 10,
})); }));

View File

@ -1,55 +1,20 @@
import { Mapper } from '@mobicoop/ddd-library';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigurationEntity } from './core/domain/configuration.entity';
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto'; import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
import { ConfigurationDomain } from './core/domain/configuration.types';
import { import {
ConfigurationReadModel, ConfigurationIdentifier,
ConfigurationWriteModel, ConfigurationValue,
} from './infrastructure/configuration.repository'; } from '@mobicoop/configuration-module';
import { v4 } from 'uuid';
@Injectable() @Injectable()
export class ConfigurationMapper export class ConfigurationMapper {
implements toResponse = (
Mapper< configurationIdentifier: ConfigurationIdentifier,
ConfigurationEntity, configurationValue: ConfigurationValue,
ConfigurationReadModel, ): ConfigurationResponseDto => {
ConfigurationWriteModel, const response = new ConfigurationResponseDto();
ConfigurationResponseDto response.domain = configurationIdentifier.domain;
> response.key = configurationIdentifier.key;
{ response.value = configurationValue;
toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
const copy = entity.getProps();
const record: ConfigurationWriteModel = {
key: `${copy.identifier.domain}:${copy.identifier.key}`,
value: copy.value,
};
return record;
};
toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
const entity = new ConfigurationEntity({
id: v4(),
createdAt: new Date(),
updatedAt: new Date(),
props: {
identifier: {
domain: record.key.split(':')[0] as ConfigurationDomain,
key: record.key.split(':')[1],
},
value: record.value,
},
});
return entity;
};
toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
const props = entity.getProps();
const response = new ConfigurationResponseDto(entity);
response.domain = props.identifier.domain;
response.key = props.identifier.key;
response.value = props.value;
return response; return response;
}; };
} }

View File

@ -7,7 +7,7 @@ import { GetConfigurationQueryHandler } from './core/application/queries/get-con
import { ConfigurationMapper } from './configuration.mapper'; import { ConfigurationMapper } from './configuration.mapper';
import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens';
import { PopulateService } from './core/application/services/populate.service'; import { PopulateService } from './core/application/services/populate.service';
import { ConfigurationRepository } from './infrastructure/configuration.repository'; import { ConfigurationRepository } from '@mobicoop/configuration-module';
const grpcControllers = [ const grpcControllers = [
GetConfigurationGrpcController, GetConfigurationGrpcController,

View File

@ -2,18 +2,23 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { SetConfigurationCommand } from './set-configuration.command'; import { SetConfigurationCommand } from './set-configuration.command';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; import {
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port'; ConfigurationDomain,
ConfigurationIdentifier,
SetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@CommandHandler(SetConfigurationCommand) @CommandHandler(SetConfigurationCommand)
export class SetConfigurationService implements ICommandHandler { export class SetConfigurationService implements ICommandHandler {
constructor( constructor(
@Inject(CONFIGURATION_REPOSITORY) @Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort, private readonly configurationRepository: SetConfigurationRepositoryPort,
) {} ) {}
async execute(command: SetConfigurationCommand): Promise<void> { async execute(
await this.configurationRepository.set( command: SetConfigurationCommand,
): Promise<ConfigurationIdentifier> {
return await this.configurationRepository.set(
{ {
domain: command.domain as ConfigurationDomain, domain: command.domain as ConfigurationDomain,
key: command.key, key: command.key,

View File

@ -1,13 +0,0 @@
import { ConfigurationEntity } from '../../domain/configuration.entity';
import {
ConfigurationIdentifier,
ConfigurationValue,
} from '../../domain/configuration.types';
export interface ConfigurationRepositoryPort {
get(identifier: ConfigurationIdentifier): Promise<ConfigurationEntity>;
set(
identifier: ConfigurationIdentifier,
value: ConfigurationValue,
): Promise<void>;
}

View File

@ -2,17 +2,19 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetConfigurationQuery } from './get-configuration.query'; import { GetConfigurationQuery } from './get-configuration.query';
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; import {
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port'; ConfigurationDomain,
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; ConfigurationValue,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@QueryHandler(GetConfigurationQuery) @QueryHandler(GetConfigurationQuery)
export class GetConfigurationQueryHandler implements IQueryHandler { export class GetConfigurationQueryHandler implements IQueryHandler {
constructor( constructor(
@Inject(CONFIGURATION_REPOSITORY) @Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort, private readonly configurationRepository: GetConfigurationRepositoryPort,
) {} ) {}
async execute(query: GetConfigurationQuery): Promise<ConfigurationEntity> { async execute(query: GetConfigurationQuery): Promise<ConfigurationValue> {
return await this.configurationRepository.get({ return await this.configurationRepository.get({
domain: query.domain as ConfigurationDomain, domain: query.domain as ConfigurationDomain,
key: query.key, key: query.key,

View File

@ -1,19 +1,23 @@
import {
ConfigurationDomain,
GetConfigurationRepositoryPort,
NotFoundException,
SetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common'; import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { ConfigurationRepositoryPort } from '../ports/configuration.repository.port';
import { import { CarpoolConfig } from '@src/config/carpool.config';
CarpoolConfig, import { PaginationConfig } from '@src/config/pagination.config';
ConfigurationDomain,
PaginationConfig,
} from '../../domain/configuration.types';
import { NotFoundException } from '@mobicoop/ddd-library';
@Injectable() @Injectable()
export class PopulateService implements OnApplicationBootstrap { export class PopulateService implements OnApplicationBootstrap {
constructor( constructor(
@Inject(CONFIGURATION_REPOSITORY) @Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort, private readonly getConfigurationRepository: GetConfigurationRepositoryPort,
@Inject(CONFIGURATION_REPOSITORY)
private readonly setConfigurationRepository: SetConfigurationRepositoryPort,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) {} ) {}
@ -42,13 +46,13 @@ export class PopulateService implements OnApplicationBootstrap {
let key: keyof typeof config; let key: keyof typeof config;
for (key in config) { for (key in config) {
try { try {
await this.configurationRepository.get({ await this.getConfigurationRepository.get({
domain, domain,
key, key,
}); });
} catch (error: any) { } catch (error: any) {
if (error instanceof NotFoundException) { if (error instanceof NotFoundException) {
this.configurationRepository.set( this.setConfigurationRepository.set(
{ {
domain, domain,
key, key,

View File

@ -1,21 +0,0 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import {
ConfigurationProps,
CreateConfigurationProps,
} from './configuration.types';
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 });
return configuration;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
}

View File

@ -1,36 +0,0 @@
// All properties that a Configuration has
export interface ConfigurationProps {
identifier: ConfigurationIdentifier;
value: ConfigurationValue;
}
// Properties that are needed for a Configuration creation
export interface CreateConfigurationProps {
identifier: ConfigurationIdentifier;
value: ConfigurationValue;
}
export enum ConfigurationDomain {
CARPOOL = 'CARPOOL',
PAGINATION = 'PAGINATION',
}
export type ConfigurationIdentifier = {
domain: ConfigurationDomain;
key: ConfigurationKey;
};
export type ConfigurationKey = string;
export type ConfigurationValue = string;
export interface CarpoolConfig {
departureTimeMargin: number;
role: string;
seatsProposed: number;
seatsRequested: number;
strictFrequency: boolean;
}
export interface PaginationConfig {
perPage: number;
}

View File

@ -1,47 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
import { InjectRedis } from '@songkeys/nestjs-redis';
import { Redis } from 'ioredis';
import {
ConfigurationIdentifier,
ConfigurationValue,
} from '../core/domain/configuration.types';
import { ConfigurationEntity } from '../core/domain/configuration.entity';
import { ConfigurationMapper } from '../configuration.mapper';
import { NotFoundException } from '@mobicoop/ddd-library';
export type ConfigurationReadModel = {
key: string;
value: string;
};
export type ConfigurationWriteModel = ConfigurationReadModel;
@Injectable()
export class ConfigurationRepository implements ConfigurationRepositoryPort {
constructor(
@InjectRedis() private readonly redis: Redis,
private readonly mapper: ConfigurationMapper,
) {}
get = async (
identifier: ConfigurationIdentifier,
): Promise<ConfigurationEntity> => {
const key: string = `${identifier.domain}:${identifier.key}`;
const value: ConfigurationValue | null = await this.redis.get(key);
if (!value)
throw new NotFoundException(
`Configuration item not found for key ${key}`,
);
return this.mapper.toDomain({
key,
value,
});
};
set = async (
identifier: ConfigurationIdentifier,
value: ConfigurationValue,
): Promise<void> => {
await this.redis.set(`${identifier.domain}:${identifier.key}`, value);
};
}

View File

@ -1,6 +1,4 @@
import { ResponseBase } from '@mobicoop/ddd-library'; export class ConfigurationResponseDto {
export class ConfigurationResponseDto extends ResponseBase {
domain: string; domain: string;
key: string; key: string;
value: string; value: string;

View File

@ -1,4 +1,4 @@
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class GetConfigurationRequestDto { export class GetConfigurationRequestDto {

View File

@ -1,4 +1,4 @@
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class SetConfigurationRequestDto { export class SetConfigurationRequestDto {

View File

@ -1,15 +1,17 @@
import { Controller, UsePipes } from '@nestjs/common'; import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs'; import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; 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 { GRPC_SERVICE_NAME } from '@src/app.constants';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { GetConfigurationRequestDto } from './dtos/get-configuration.request.dto'; import { GetConfigurationRequestDto } from './dtos/get-configuration.request.dto';
import { ConfigurationResponseDto } from '../dtos/configuration.response.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'; import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
import {
NotFoundException,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { ConfigurationValue } from '@mobicoop/configuration-module';
@UsePipes( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({
@ -29,10 +31,11 @@ export class GetConfigurationGrpcController {
data: GetConfigurationRequestDto, data: GetConfigurationRequestDto,
): Promise<ConfigurationResponseDto> { ): Promise<ConfigurationResponseDto> {
try { try {
const configuration: ConfigurationEntity = await this.queryBus.execute( const configurationValue: ConfigurationValue =
await this.queryBus.execute(
new GetConfigurationQuery(data.domain, data.key), new GetConfigurationQuery(data.domain, data.key),
); );
return this.mapper.toResponse(configuration); return this.mapper.toResponse(data, configurationValue);
} catch (e) { } catch (e) {
if (e instanceof NotFoundException) { if (e instanceof NotFoundException) {
throw new RpcException({ throw new RpcException({

View File

@ -1,11 +1,11 @@
import { Controller, UsePipes } from '@nestjs/common'; import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { RpcExceptionCode, RpcValidationPipe } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { GRPC_SERVICE_NAME } from '@src/app.constants'; import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { SetConfigurationRequestDto } from './dtos/set-configuration.request.dto'; import { SetConfigurationRequestDto } from './dtos/set-configuration.request.dto';
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command'; import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
import { ConfigurationIdentifier } from '@mobicoop/configuration-module';
@UsePipes( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({
@ -20,11 +20,13 @@ export class SetConfigurationGrpcController {
@GrpcMethod(GRPC_SERVICE_NAME, 'Set') @GrpcMethod(GRPC_SERVICE_NAME, 'Set')
async set( async set(
setConfigurationRequestDto: SetConfigurationRequestDto, setConfigurationRequestDto: SetConfigurationRequestDto,
): Promise<void> { ): Promise<ConfigurationIdentifier> {
try { try {
const configurationIdentifier: ConfigurationIdentifier =
await this.commandBus.execute( await this.commandBus.execute(
new SetConfigurationCommand(setConfigurationRequestDto), new SetConfigurationCommand(setConfigurationRequestDto),
); );
return configurationIdentifier;
} catch (error: any) { } catch (error: any) {
throw new RpcException({ throw new RpcException({
code: RpcExceptionCode.UNKNOWN, code: RpcExceptionCode.UNKNOWN,

View File

@ -1,31 +1,8 @@
import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } 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 { ConfigurationResponseDto } from '@modules/configuration/interface/dtos/configuration.response.dto';
import { Test } from '@nestjs/testing'; 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: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
},
createdAt: now,
updatedAt: now,
});
const configurationReadModel: ConfigurationReadModel = {
key: 'AD:seatsProposed',
value: '4',
};
describe('Configuration Mapper', () => { describe('Configuration Mapper', () => {
let configurationMapper: ConfigurationMapper; let configurationMapper: ConfigurationMapper;
@ -40,22 +17,16 @@ describe('Configuration Mapper', () => {
expect(configurationMapper).toBeDefined(); expect(configurationMapper).toBeDefined();
}); });
it('should map domain entity to persistence data', async () => { it('should map configuration to response', async () => {
const mapped: ConfigurationWriteModel = const mapped: ConfigurationResponseDto = configurationMapper.toResponse(
configurationMapper.toPersistence(configurationEntity); {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
'3',
);
expect(mapped.domain).toBe('CARPOOL');
expect(mapped.key).toBe('seatsProposed');
expect(mapped.value).toBe('3'); 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');
});
}); });

View File

@ -1,23 +0,0 @@
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import {
CreateConfigurationProps,
ConfigurationDomain,
} from '@modules/configuration/core/domain/configuration.types';
const createConfigurationProps: CreateConfigurationProps = {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
};
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');
});
});

View File

@ -1,26 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler'; import { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query'; import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
import {
ConfigurationDomain,
ConfigurationValue,
} from '@mobicoop/configuration-module';
const now = new Date('2023-06-21 06:00:00'); const configurationValue: ConfigurationValue = '3';
const configuration: ConfigurationEntity = new ConfigurationEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
},
createdAt: now,
updatedAt: now,
});
const mockConfigurationRepository = { const mockConfigurationRepository = {
get: jest.fn().mockImplementation(() => configuration), get: jest.fn().mockImplementation(() => configurationValue),
}; };
describe('Get Configuration Query Handler', () => { describe('Get Configuration Query Handler', () => {
@ -47,14 +37,14 @@ describe('Get Configuration Query Handler', () => {
}); });
describe('execution', () => { describe('execution', () => {
it('should return a configuration item', async () => { it('should return a configuration value', async () => {
const getConfigurationQuery = new GetConfigurationQuery( const getConfigurationQuery = new GetConfigurationQuery(
ConfigurationDomain.CARPOOL, ConfigurationDomain.CARPOOL,
'seatsProposed', 'seatsProposed',
); );
const configuration: ConfigurationEntity = const configurationValue: ConfigurationValue =
await getConfigurationQueryHandler.execute(getConfigurationQuery); await getConfigurationQueryHandler.execute(getConfigurationQuery);
expect(configuration.getProps().value).toBe('3'); expect(configurationValue).toBe('3');
}); });
}); });
}); });

View File

@ -1,29 +1,13 @@
import { NotFoundException } from '@mobicoop/ddd-library'; import { NotFoundException } from '@mobicoop/configuration-module';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { PopulateService } from '@modules/configuration/core/application/services/populate.service'; import { PopulateService } from '@modules/configuration/core/application/services/populate.service';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
const mockConfigurationRepository = { const mockConfigurationRepository = {
get: jest get: jest
.fn() .fn()
.mockImplementationOnce( .mockImplementationOnce(() => 'someValue')
() =>
new ConfigurationEntity({
id: '001199d4-7187-4e83-a044-12159cba2e33',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'someKey',
},
value: 'someValue',
},
createdAt: new Date('2023-10-23'),
updatedAt: new Date('2023-10-23'),
}),
)
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
throw new NotFoundException('Configuration not found'); throw new NotFoundException('Configuration not found');
}), }),

View File

@ -1,10 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto'; import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service'; import { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens'; import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command'; import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; import { ConfigurationDomain } from '@mobicoop/configuration-module';
const setConfigurationRequest: SetConfigurationRequestDto = { const setConfigurationRequest: SetConfigurationRequestDto = {
domain: ConfigurationDomain.CARPOOL, domain: ConfigurationDomain.CARPOOL,
@ -50,16 +49,10 @@ describe('Set Configuration Service', () => {
); );
it('should set an existing configuration item', async () => { it('should set an existing configuration item', async () => {
jest.spyOn(mockConfigurationRepository, 'set'); jest.spyOn(mockConfigurationRepository, 'set');
ConfigurationEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
await setConfigurationService.execute(setConfigurationCommand); await setConfigurationService.execute(setConfigurationCommand);
expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1); expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1);
}); });
it('should throw an error if something bad happens on configuration item update', async () => { 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( await expect(
setConfigurationService.execute(setConfigurationCommand), setConfigurationService.execute(setConfigurationCommand),
).rejects.toBeInstanceOf(Error); ).rejects.toBeInstanceOf(Error);

View File

@ -1,93 +0,0 @@
import { NotFoundException } from '@mobicoop/ddd-library';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
import { Test, TestingModule } from '@nestjs/testing';
import { getRedisToken } from '@songkeys/nestjs-redis';
const mockRedis = {
get: jest
.fn()
.mockImplementationOnce(() => '1')
.mockImplementation(() => null),
set: jest.fn().mockImplementation(),
};
const mockConfigurationMapper = {
toDomain: jest.fn().mockImplementation(
() =>
new ConfigurationEntity({
id: '001199d4-7187-4e83-a044-12159cba2e33',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '1',
},
createdAt: new Date('2023-10-23'),
updatedAt: new Date('2023-10-23'),
}),
),
};
describe('Configuration Repository', () => {
let configurationRepository: ConfigurationRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getRedisToken('default'),
useValue: mockRedis,
},
{
provide: ConfigurationMapper,
useValue: mockConfigurationMapper,
},
ConfigurationRepository,
],
}).compile();
configurationRepository = module.get<ConfigurationRepository>(
ConfigurationRepository,
);
});
it('should be defined', () => {
expect(configurationRepository).toBeDefined();
});
describe('interact', () => {
it('should get a value', async () => {
expect(
(
await configurationRepository.get({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
})
).getProps().value,
).toBe('1');
});
it('should throw if configuration is not found', async () => {
await expect(
configurationRepository.get({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
}),
).rejects.toBeInstanceOf(NotFoundException);
});
it('should set a value', async () => {
expect(
await configurationRepository.set(
{
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
'3',
),
).toBeUndefined();
});
});
});

View File

@ -1,7 +1,6 @@
import { NotFoundException } from '@mobicoop/ddd-library'; import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller'; import { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller';
import { QueryBus } from '@nestjs/cqrs'; import { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices'; import { RpcException } from '@nestjs/microservices';

View File

@ -1,5 +1,8 @@
import {
ConfigurationDomain,
ConfigurationIdentifier,
} from '@mobicoop/configuration-module';
import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto'; 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 { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
@ -15,7 +18,10 @@ const setConfigurationRequest: SetConfigurationRequestDto = {
const mockCommandBus = { const mockCommandBus = {
execute: jest execute: jest
.fn() .fn()
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2') .mockImplementationOnce(() => ({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
}))
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
throw new Error(); throw new Error();
}), }),
@ -50,7 +56,9 @@ describe('Set Configuration Grpc Controller', () => {
it('should set a configuration item', async () => { it('should set a configuration item', async () => {
jest.spyOn(mockCommandBus, 'execute'); jest.spyOn(mockCommandBus, 'execute');
const configurationIdentifier: ConfigurationIdentifier =
await setConfigurationGrpcController.set(setConfigurationRequest); await setConfigurationGrpcController.set(setConfigurationRequest);
expect(configurationIdentifier.key).toBe('seatsProposed');
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
}); });