Merge branch 'useLibrary' into 'main'
Use configuration package See merge request v3/service/configuration!26
This commit is contained in:
commit
09f0142325
|
@ -1,6 +1,6 @@
|
|||
# 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 :
|
||||
|
||||
|
@ -8,10 +8,6 @@ Each item consists in :
|
|||
- 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)
|
||||
|
||||
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
|
||||
|
||||
- **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...).
|
||||
|
||||
A RabbitMQ instance is also required to send / receive messages when data has been inserted/updated/deleted.
|
||||
|
||||
## Installation
|
||||
|
||||
- copy `.env.dist` to `.env` :
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/configuration",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "Mobicoop V3 Configuration Service",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
|
@ -26,6 +26,7 @@
|
|||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.6",
|
||||
"@grpc/proto-loader": "^0.7.10",
|
||||
"@mobicoop/configuration-module": "^4.0.0",
|
||||
"@mobicoop/ddd-library": "^2.1.1",
|
||||
"@mobicoop/health-module": "^2.3.1",
|
||||
"@mobicoop/message-broker-module": "^2.1.1",
|
||||
|
@ -37,14 +38,10 @@
|
|||
"@nestjs/microservices": "^10.2.7",
|
||||
"@nestjs/platform-express": "^10.2.7",
|
||||
"@nestjs/terminus": "^10.1.1",
|
||||
"@songkeys/nestjs-redis": "^10.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^5.0.5",
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^9.0.1"
|
||||
"rimraf": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
|
@ -54,7 +51,6 @@
|
|||
"@types/jest": "29.5.6",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/supertest": "^2.0.15",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
|
|
|
@ -4,6 +4,10 @@ import { Module } from '@nestjs/common';
|
|||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
|
||||
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 { ConfigurationModule } from '@modules/configuration/configuration.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
@ -11,7 +15,6 @@ import brokerConfig from './config/broker.config';
|
|||
import carpoolConfig from './config/carpool.config';
|
||||
import paginationConfig from './config/pagination.config';
|
||||
import serviceConfig from './config/service.config';
|
||||
import { RedisModule, RedisModuleOptions } from '@songkeys/nestjs-redis';
|
||||
import redisConfig from './config/redis.config';
|
||||
import { Transport } from '@nestjs/microservices';
|
||||
|
||||
|
@ -49,18 +52,16 @@ import { Transport } from '@nestjs/microservices';
|
|||
],
|
||||
}),
|
||||
}),
|
||||
RedisModule.forRootAsync({
|
||||
ConfigurationModulePackage.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<RedisModuleOptions> => {
|
||||
): Promise<ConfigurationModuleOptions> => {
|
||||
return {
|
||||
config: {
|
||||
host: configService.get<string>('redis.host') as string,
|
||||
port: configService.get<number>('redis.port') as number,
|
||||
password: configService.get<string>('redis.password'),
|
||||
},
|
||||
host: configService.get<string>('redis.host') as string,
|
||||
port: configService.get<number>('redis.port') as number,
|
||||
password: configService.get<string>('redis.password'),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export interface CarpoolConfig {
|
||||
departureTimeMargin: number;
|
||||
role: string;
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strictFrequency: boolean;
|
||||
}
|
||||
|
||||
export default registerAs('carpool', () => ({
|
||||
departureTimeMargin: process.env.DEPARTURE_TIME_MARGIN
|
||||
? parseInt(process.env.DEPARTURE_TIME_MARGIN, 10)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export interface PaginationConfig {
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export default registerAs('pagination', () => ({
|
||||
perPage: process.env.PER_PAGE ? parseInt(process.env.PER_PAGE, 10) : 10,
|
||||
}));
|
||||
|
|
|
@ -1,55 +1,20 @@
|
|||
import { Mapper } from '@mobicoop/ddd-library';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationEntity } from './core/domain/configuration.entity';
|
||||
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
|
||||
import { ConfigurationDomain } from './core/domain/configuration.types';
|
||||
import {
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel,
|
||||
} from './infrastructure/configuration.repository';
|
||||
import { v4 } from 'uuid';
|
||||
ConfigurationIdentifier,
|
||||
ConfigurationValue,
|
||||
} from '@mobicoop/configuration-module';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationMapper
|
||||
implements
|
||||
Mapper<
|
||||
ConfigurationEntity,
|
||||
ConfigurationReadModel,
|
||||
ConfigurationWriteModel,
|
||||
ConfigurationResponseDto
|
||||
>
|
||||
{
|
||||
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;
|
||||
export class ConfigurationMapper {
|
||||
toResponse = (
|
||||
configurationIdentifier: ConfigurationIdentifier,
|
||||
configurationValue: ConfigurationValue,
|
||||
): ConfigurationResponseDto => {
|
||||
const response = new ConfigurationResponseDto();
|
||||
response.domain = configurationIdentifier.domain;
|
||||
response.key = configurationIdentifier.key;
|
||||
response.value = configurationValue;
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { GetConfigurationQueryHandler } from './core/application/queries/get-con
|
|||
import { ConfigurationMapper } from './configuration.mapper';
|
||||
import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens';
|
||||
import { PopulateService } from './core/application/services/populate.service';
|
||||
import { ConfigurationRepository } from './infrastructure/configuration.repository';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
|
||||
const grpcControllers = [
|
||||
GetConfigurationGrpcController,
|
||||
|
|
|
@ -2,18 +2,23 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { SetConfigurationCommand } from './set-configuration.command';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import {
|
||||
ConfigurationDomain,
|
||||
ConfigurationIdentifier,
|
||||
SetConfigurationRepositoryPort,
|
||||
} from '@mobicoop/configuration-module';
|
||||
|
||||
@CommandHandler(SetConfigurationCommand)
|
||||
export class SetConfigurationService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||
private readonly configurationRepository: SetConfigurationRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: SetConfigurationCommand): Promise<void> {
|
||||
await this.configurationRepository.set(
|
||||
async execute(
|
||||
command: SetConfigurationCommand,
|
||||
): Promise<ConfigurationIdentifier> {
|
||||
return await this.configurationRepository.set(
|
||||
{
|
||||
domain: command.domain as ConfigurationDomain,
|
||||
key: command.key,
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -2,17 +2,19 @@ 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 { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
|
||||
import {
|
||||
ConfigurationDomain,
|
||||
ConfigurationValue,
|
||||
GetConfigurationRepositoryPort,
|
||||
} from '@mobicoop/configuration-module';
|
||||
|
||||
@QueryHandler(GetConfigurationQuery)
|
||||
export class GetConfigurationQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@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({
|
||||
domain: query.domain as ConfigurationDomain,
|
||||
key: query.key,
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import {
|
||||
ConfigurationDomain,
|
||||
GetConfigurationRepositoryPort,
|
||||
NotFoundException,
|
||||
SetConfigurationRepositoryPort,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigurationRepositoryPort } from '../ports/configuration.repository.port';
|
||||
import {
|
||||
CarpoolConfig,
|
||||
ConfigurationDomain,
|
||||
PaginationConfig,
|
||||
} from '../../domain/configuration.types';
|
||||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
|
||||
import { CarpoolConfig } from '@src/config/carpool.config';
|
||||
import { PaginationConfig } from '@src/config/pagination.config';
|
||||
|
||||
@Injectable()
|
||||
export class PopulateService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||
private readonly getConfigurationRepository: GetConfigurationRepositoryPort,
|
||||
@Inject(CONFIGURATION_REPOSITORY)
|
||||
private readonly setConfigurationRepository: SetConfigurationRepositoryPort,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
|
@ -42,13 +46,13 @@ export class PopulateService implements OnApplicationBootstrap {
|
|||
let key: keyof typeof config;
|
||||
for (key in config) {
|
||||
try {
|
||||
await this.configurationRepository.get({
|
||||
await this.getConfigurationRepository.get({
|
||||
domain,
|
||||
key,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException) {
|
||||
this.configurationRepository.set(
|
||||
this.setConfigurationRepository.set(
|
||||
{
|
||||
domain,
|
||||
key,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ConfigurationResponseDto extends ResponseBase {
|
||||
export class ConfigurationResponseDto {
|
||||
domain: string;
|
||||
key: string;
|
||||
value: string;
|
||||
|
|
|
@ -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';
|
||||
|
||||
export class GetConfigurationRequestDto {
|
||||
|
|
|
@ -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';
|
||||
|
||||
export class SetConfigurationRequestDto {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
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';
|
||||
import {
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { ConfigurationValue } from '@mobicoop/configuration-module';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
|
@ -29,10 +31,11 @@ export class GetConfigurationGrpcController {
|
|||
data: GetConfigurationRequestDto,
|
||||
): Promise<ConfigurationResponseDto> {
|
||||
try {
|
||||
const configuration: ConfigurationEntity = await this.queryBus.execute(
|
||||
new GetConfigurationQuery(data.domain, data.key),
|
||||
);
|
||||
return this.mapper.toResponse(configuration);
|
||||
const configurationValue: ConfigurationValue =
|
||||
await this.queryBus.execute(
|
||||
new GetConfigurationQuery(data.domain, data.key),
|
||||
);
|
||||
return this.mapper.toResponse(data, configurationValue);
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundException) {
|
||||
throw new RpcException({
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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 { RpcExceptionCode, 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';
|
||||
import { ConfigurationIdentifier } from '@mobicoop/configuration-module';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
|
@ -20,11 +20,13 @@ export class SetConfigurationGrpcController {
|
|||
@GrpcMethod(GRPC_SERVICE_NAME, 'Set')
|
||||
async set(
|
||||
setConfigurationRequestDto: SetConfigurationRequestDto,
|
||||
): Promise<void> {
|
||||
): Promise<ConfigurationIdentifier> {
|
||||
try {
|
||||
await this.commandBus.execute(
|
||||
new SetConfigurationCommand(setConfigurationRequestDto),
|
||||
);
|
||||
const configurationIdentifier: ConfigurationIdentifier =
|
||||
await this.commandBus.execute(
|
||||
new SetConfigurationCommand(setConfigurationRequestDto),
|
||||
);
|
||||
return configurationIdentifier;
|
||||
} catch (error: any) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
|
|
|
@ -1,31 +1,8 @@
|
|||
import { ConfigurationDomain } from '@mobicoop/configuration-module';
|
||||
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 { 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', () => {
|
||||
let configurationMapper: ConfigurationMapper;
|
||||
|
||||
|
@ -40,22 +17,16 @@ describe('Configuration Mapper', () => {
|
|||
expect(configurationMapper).toBeDefined();
|
||||
});
|
||||
|
||||
it('should map domain entity to persistence data', async () => {
|
||||
const mapped: ConfigurationWriteModel =
|
||||
configurationMapper.toPersistence(configurationEntity);
|
||||
it('should map configuration to response', async () => {
|
||||
const mapped: ConfigurationResponseDto = configurationMapper.toResponse(
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
key: 'seatsProposed',
|
||||
},
|
||||
'3',
|
||||
);
|
||||
expect(mapped.domain).toBe('CARPOOL');
|
||||
expect(mapped.key).toBe('seatsProposed');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -1,26 +1,16 @@
|
|||
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 { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||
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 configuration: ConfigurationEntity = new ConfigurationEntity({
|
||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
props: {
|
||||
identifier: {
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
key: 'seatsProposed',
|
||||
},
|
||||
value: '3',
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const configurationValue: ConfigurationValue = '3';
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
get: jest.fn().mockImplementation(() => configuration),
|
||||
get: jest.fn().mockImplementation(() => configurationValue),
|
||||
};
|
||||
|
||||
describe('Get Configuration Query Handler', () => {
|
||||
|
@ -47,14 +37,14 @@ describe('Get Configuration Query Handler', () => {
|
|||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return a configuration item', async () => {
|
||||
it('should return a configuration value', async () => {
|
||||
const getConfigurationQuery = new GetConfigurationQuery(
|
||||
ConfigurationDomain.CARPOOL,
|
||||
'seatsProposed',
|
||||
);
|
||||
const configuration: ConfigurationEntity =
|
||||
const configurationValue: ConfigurationValue =
|
||||
await getConfigurationQueryHandler.execute(getConfigurationQuery);
|
||||
expect(configuration.getProps().value).toBe('3');
|
||||
expect(configurationValue).toBe('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 { 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 { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockConfigurationRepository = {
|
||||
get: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
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(() => 'someValue')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException('Configuration not found');
|
||||
}),
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
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 { 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';
|
||||
import { ConfigurationDomain } from '@mobicoop/configuration-module';
|
||||
|
||||
const setConfigurationRequest: SetConfigurationRequestDto = {
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
|
@ -50,16 +49,10 @@ describe('Set Configuration Service', () => {
|
|||
);
|
||||
it('should set an existing configuration item', async () => {
|
||||
jest.spyOn(mockConfigurationRepository, 'set');
|
||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await setConfigurationService.execute(setConfigurationCommand);
|
||||
expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
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,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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { ConfigurationDomain } from '@mobicoop/configuration-module';
|
||||
import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
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 { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import {
|
||||
ConfigurationDomain,
|
||||
ConfigurationIdentifier,
|
||||
} from '@mobicoop/configuration-module';
|
||||
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 { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
|
@ -15,7 +18,10 @@ const setConfigurationRequest: SetConfigurationRequestDto = {
|
|||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||
.mockImplementationOnce(() => ({
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
key: 'seatsProposed',
|
||||
}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
|
@ -50,7 +56,9 @@ describe('Set Configuration Grpc Controller', () => {
|
|||
|
||||
it('should set a configuration item', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await setConfigurationGrpcController.set(setConfigurationRequest);
|
||||
const configurationIdentifier: ConfigurationIdentifier =
|
||||
await setConfigurationGrpcController.set(setConfigurationRequest);
|
||||
expect(configurationIdentifier.key).toBe('seatsProposed');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue