diff --git a/.env.dist b/.env.dist index 3063ff5..76c3a27 100644 --- a/.env.dist +++ b/.env.dist @@ -3,14 +3,15 @@ SERVICE_URL=0.0.0.0 SERVICE_PORT=5003 HEALTH_SERVICE_PORT=6003 -# PRISMA -DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration" - # MESSAGE BROKER MESSAGE_BROKER_URI=amqp://v3-broker:5672 MESSAGE_BROKER_EXCHANGE=mobicoop MESSAGE_BROKER_EXCHANGE_DURABILITY=true +# REDIS +REDIS_HOST=v3-redis +REDIS_PASSWORD=redis +REDIS_PORT=6379 # DEFAULT CONFIGURATION diff --git a/package-lock.json b/package-lock.json index a4e755d..8925f33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,10 @@ "@nestjs/platform-express": "^10.2.7", "@nestjs/terminus": "^10.1.1", "@prisma/client": "^5.4.2", + "@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", @@ -1096,6 +1098,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2490,6 +2497,27 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@songkeys/nestjs-redis": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@songkeys/nestjs-redis/-/nestjs-redis-10.0.0.tgz", + "integrity": "sha512-s56+NECuJXzcaPLYzpvA2xjL0e/1Zy55UE0q6b1UqpbQSKI06TFPFCWCMUadJigiuB26O1hxi+lmDbzahKvcLg==", + "dependencies": { + "tslib": "2.6.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "ioredis": "^5.0.0" + } + }, + "node_modules/@songkeys/nestjs-redis/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2687,12 +2715,6 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/js-yaml": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", - "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -4104,6 +4126,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4587,6 +4617,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5944,6 +5982,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6971,6 +7032,16 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8014,6 +8085,25 @@ "node": ">= 0.10" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -8538,6 +8628,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index fcad0db..86c205c 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,10 @@ "@nestjs/platform-express": "^10.2.7", "@nestjs/terminus": "^10.1.1", "@prisma/client": "^5.4.2", + "@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", @@ -91,6 +93,7 @@ ".di-tokens.ts", ".response.ts", ".port.ts", + ".config.ts", "prisma.service.ts", "main.ts" ], @@ -109,6 +112,7 @@ ".di-tokens.ts", ".response.ts", ".port.ts", + ".config.ts", "prisma.service.ts", "main.ts" ], diff --git a/prisma/migrations/20230524120120_init/migration.sql b/prisma/migrations/20230524120120_init/migration.sql deleted file mode 100644 index 8778460..0000000 --- a/prisma/migrations/20230524120120_init/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- CreateTable -CREATE TABLE "configuration" ( - "uuid" UUID NOT NULL, - "domain" TEXT NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "configuration_pkey" PRIMARY KEY ("uuid") -); - --- CreateIndex -CREATE UNIQUE INDEX "configuration_domain_key_key" ON "configuration"("domain", "key"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 4c80545..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,24 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - binaryTargets = ["linux-musl", "debian-openssl-3.0.x"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model Configuration { - uuid String @id @default(uuid()) @db.Uuid - domain String - key String - value String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([domain, key]) - @@map("configuration") -} diff --git a/src/app.module.ts b/src/app.module.ts index 8c0e669..d7c0def 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,7 @@ import { } from '@mobicoop/health-module'; import { MessagerModule } from '@modules/messager/messager.module'; import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { HEALTH_CONFIGURATION_REPOSITORY, HEALTH_CRITICAL_LOGGING_KEY, @@ -18,9 +18,10 @@ import { ConfigurationModule } from '@modules/configuration/configuration.module import { EventEmitterModule } from '@nestjs/event-emitter'; import brokerConfig from './config/broker.config'; import carpoolConfig from './config/carpool.config'; -import databaseConfig from './config/database.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'; @Module({ imports: [ @@ -29,8 +30,8 @@ import serviceConfig from './config/service.config'; load: [ brokerConfig, carpoolConfig, - databaseConfig, paginationConfig, + redisConfig, serviceConfig, ], }), @@ -53,6 +54,21 @@ import serviceConfig from './config/service.config'; messagePublisher, }), }), + RedisModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async ( + configService: ConfigService, + ): Promise => { + return { + config: { + host: configService.get('redis.host') as string, + port: configService.get('redis.port') as number, + password: configService.get('redis.password'), + }, + }; + }, + }), ConfigurationModule, MessagerModule, ], diff --git a/src/config/database.config.ts b/src/config/database.config.ts deleted file mode 100644 index 116843a..0000000 --- a/src/config/database.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export default registerAs('database', () => ({ - url: - process.env.DATABASE_URL ?? - 'postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration', -})); diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts new file mode 100644 index 0000000..ff4acd1 --- /dev/null +++ b/src/config/redis.config.ts @@ -0,0 +1,7 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('redis', () => ({ + host: process.env.REDIS_HOST ?? 'v3-redis', + port: process.env.PORT ? parseInt(process.env.PORT, 10) : 6379, + password: process.env.REDIS_PASSWORD ?? 'redis', +})); diff --git a/src/modules/configuration/configuration.mapper.ts b/src/modules/configuration/configuration.mapper.ts index 86767c0..726f814 100644 --- a/src/modules/configuration/configuration.mapper.ts +++ b/src/modules/configuration/configuration.mapper.ts @@ -1,22 +1,13 @@ 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 { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto'; -import { - ConfigurationDomain, - ConfigurationType, -} from './core/domain/configuration.types'; - -/** - * 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). - */ +import { v4 } from 'uuid'; @Injectable() export class ConfigurationMapper @@ -31,27 +22,23 @@ export class ConfigurationMapper toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => { const copy = entity.getProps(); const record: ConfigurationWriteModel = { - uuid: entity.id, - domain: copy.identifier.domain, - key: copy.identifier.key, + key: `${copy.identifier.domain}:${copy.identifier.key}`, value: copy.value, - type: copy.type, }; return record; }; toDomain = (record: ConfigurationReadModel): ConfigurationEntity => { const entity = new ConfigurationEntity({ - id: record.uuid, - createdAt: new Date(record.createdAt), - updatedAt: new Date(record.updatedAt), + id: v4(), + createdAt: new Date(), + updatedAt: new Date(), props: { identifier: { - domain: record.domain as ConfigurationDomain, - key: record.key, + domain: record.key.split(':')[0] as ConfigurationDomain, + key: record.key.split(':')[1], }, value: record.value, - type: record.type as ConfigurationType, }, }); return entity; @@ -63,7 +50,6 @@ export class ConfigurationMapper response.domain = props.identifier.domain; response.key = props.identifier.key; response.value = props.value; - response.type = props.type; return response; }; } diff --git a/src/modules/configuration/configuration.module.ts b/src/modules/configuration/configuration.module.ts index 687e5d4..049d77d 100644 --- a/src/modules/configuration/configuration.module.ts +++ b/src/modules/configuration/configuration.module.ts @@ -2,45 +2,30 @@ import { Module, Provider } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller'; import { SetConfigurationGrpcController } from './interface/grpc-controllers/set-configuration.grpc.controller'; -import { DeleteConfigurationGrpcController } from './interface/grpc-controllers/delete-configuration.grpc.controller'; -import { PropagateConfigurationsGrpcController } from './interface/grpc-controllers/propagate-configurations.grpc.controller'; -import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler'; -import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler'; import { SetConfigurationService } from './core/application/commands/set-configuration/set-configuration.service'; -import { DeleteConfigurationService } from './core/application/commands/delete-configuration/delete-configuration.service'; import { GetConfigurationQueryHandler } from './core/application/queries/get-configuration/get-configuration.query-handler'; import { ConfigurationMapper } from './configuration.mapper'; import { CONFIGURATION_MESSAGE_PUBLISHER, CONFIGURATION_REPOSITORY, } from './configuration.di-tokens'; -import { ConfigurationRepository } from './infrastructure/configuration.repository'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; -import { PrismaService } from './infrastructure/prisma.service'; -import { PropagateConfigurationsService } from './core/application/commands/propagate-configurations/propagate-configurations.service'; +import { PopulateService } from './core/application/services/populate.service'; +import { ConfigurationRepository } from './infrastructure/configuration.repository'; const grpcControllers = [ GetConfigurationGrpcController, SetConfigurationGrpcController, - DeleteConfigurationGrpcController, - PropagateConfigurationsGrpcController, ]; -const eventHandlers: Provider[] = [ - PublishMessageWhenConfigurationIsSetDomainEventHandler, - PublishMessageWhenConfigurationIsDeletedDomainEventHandler, -]; - -const commandHandlers: Provider[] = [ - SetConfigurationService, - DeleteConfigurationService, - PropagateConfigurationsService, -]; +const commandHandlers: Provider[] = [SetConfigurationService]; const queryHandlers: Provider[] = [GetConfigurationQueryHandler]; const mappers: Provider[] = [ConfigurationMapper]; +const providers: Provider[] = [PopulateService]; + const repositories: Provider[] = [ { provide: CONFIGURATION_REPOSITORY, @@ -54,20 +39,18 @@ const messagePublishers: Provider[] = [ useExisting: MessageBrokerPublisher, }, ]; -const orms: Provider[] = [PrismaService]; @Module({ imports: [CqrsModule], controllers: [...grpcControllers], providers: [ - ...eventHandlers, ...commandHandlers, ...queryHandlers, ...mappers, + ...providers, ...repositories, ...messagePublishers, - ...orms, ], - exports: [PrismaService, ConfigurationMapper, CONFIGURATION_REPOSITORY], + exports: [ConfigurationMapper, CONFIGURATION_REPOSITORY], }) export class ConfigurationModule {} diff --git a/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.command.ts b/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.command.ts deleted file mode 100644 index d22c4af..0000000 --- a/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.command.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Command, CommandProps } from '@mobicoop/ddd-library'; - -export class DeleteConfigurationCommand extends Command { - readonly domain: string; - readonly key: string; - - constructor(props: CommandProps) { - super(props); - this.domain = props.domain; - this.key = props.key; - } -} diff --git a/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.service.ts b/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.service.ts deleted file mode 100644 index 52755de..0000000 --- a/src/modules/configuration/core/application/commands/delete-configuration/delete-configuration.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 { - 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; - } -} diff --git a/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.command.ts b/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.command.ts deleted file mode 100644 index 79d2619..0000000 --- a/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command, CommandProps } from '@mobicoop/ddd-library'; - -export class PropagateConfigurationsCommand extends Command { - constructor(props: CommandProps) { - super(props); - } -} diff --git a/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.service.ts b/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.service.ts deleted file mode 100644 index 248e31f..0000000 --- a/src/modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 { - const configurationItems: ConfigurationEntity[] = - await this.repository.findAll({}); - this.messagePublisher.publish( - CONFIGURATION_PROPAGATED_ROUTING_KEY, - JSON.stringify( - configurationItems.map((configuration: ConfigurationEntity) => - this.configurationMapper.toResponse(configuration), - ), - ), - ); - } -} diff --git a/src/modules/configuration/core/application/commands/set-configuration/set-configuration.service.ts b/src/modules/configuration/core/application/commands/set-configuration/set-configuration.service.ts index f1faa3d..ae488cb 100644 --- a/src/modules/configuration/core/application/commands/set-configuration/set-configuration.service.ts +++ b/src/modules/configuration/core/application/commands/set-configuration/set-configuration.service.ts @@ -1,53 +1,24 @@ 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 { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port'; -import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; -import { - ConfigurationDomain, - ConfigurationType, -} from '@modules/configuration/core/domain/configuration.types'; @CommandHandler(SetConfigurationCommand) export class SetConfigurationService implements ICommandHandler { constructor( @Inject(CONFIGURATION_REPOSITORY) - private readonly repository: ConfigurationRepositoryPort, + private readonly configurationRepository: ConfigurationRepositoryPort, ) {} - async execute(command: SetConfigurationCommand): Promise { - 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({ - identifier: { - domain: command.domain as ConfigurationDomain, - key: command.key, - }, - value: command.value, - type: ConfigurationType.STRING, - }); - await this.repository.insert(newConfiguration); - return newConfiguration.id; - } catch (error: any) { - throw error; - } - } - throw error; - } + async execute(command: SetConfigurationCommand): Promise { + await this.configurationRepository.set( + { + domain: command.domain as ConfigurationDomain, + key: command.key, + }, + command.value, + ); } } diff --git a/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler.ts b/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler.ts deleted file mode 100644 index b7c0f65..0000000 --- a/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 { - const configurationDeletedIntegrationEvent = - new ConfigurationDeletedIntegrationEvent({ - id: event.aggregateId, - domain: event.identifier.domain, - key: event.identifier.key, - metadata: event.metadata, - }); - this.messagePublisher.publish( - CONFIGURATION_DELETED_ROUTING_KEY, - JSON.stringify(configurationDeletedIntegrationEvent), - ); - } -} diff --git a/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler.ts b/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler.ts deleted file mode 100644 index 4a8672a..0000000 --- a/src/modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 { - const configurationSetIntegrationEvent = - new ConfigurationSetIntegrationEvent({ - id: event.aggregateId, - domain: event.identifier.domain, - key: event.identifier.key, - value: event.value, - metadata: event.metadata, - }); - this.messagePublisher.publish( - CONFIGURATION_SET_ROUTING_KEY, - JSON.stringify(configurationSetIntegrationEvent), - ); - } -} diff --git a/src/modules/configuration/core/application/events/configuration-deleted.integration-event.ts b/src/modules/configuration/core/application/events/configuration-deleted.integration-event.ts deleted file mode 100644 index 517edcc..0000000 --- a/src/modules/configuration/core/application/events/configuration-deleted.integration-event.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library'; - -export class ConfigurationDeletedIntegrationEvent extends IntegrationEvent { - readonly domain: string; - readonly key: string; - - constructor( - props: IntegrationEventProps, - ) { - super(props); - this.domain = props.domain; - this.key = props.key; - } -} diff --git a/src/modules/configuration/core/application/events/configuration-set.integration-event.ts b/src/modules/configuration/core/application/events/configuration-set.integration-event.ts deleted file mode 100644 index c129da9..0000000 --- a/src/modules/configuration/core/application/events/configuration-set.integration-event.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library'; - -export class ConfigurationSetIntegrationEvent extends IntegrationEvent { - readonly domain: string; - readonly key: string; - readonly value: string; - - constructor(props: IntegrationEventProps) { - super(props); - this.domain = props.domain; - this.key = props.key; - this.value = props.value; - } -} diff --git a/src/modules/configuration/core/application/ports/configuration.repository.port.ts b/src/modules/configuration/core/application/ports/configuration.repository.port.ts index 76aa386..0485ca6 100644 --- a/src/modules/configuration/core/application/ports/configuration.repository.port.ts +++ b/src/modules/configuration/core/application/ports/configuration.repository.port.ts @@ -1,4 +1,13 @@ -import { RepositoryPort } from '@mobicoop/ddd-library'; import { ConfigurationEntity } from '../../domain/configuration.entity'; +import { + ConfigurationIdentifier, + ConfigurationValue, +} from '../../domain/configuration.types'; -export type ConfigurationRepositoryPort = RepositoryPort; +export interface ConfigurationRepositoryPort { + get(identifier: ConfigurationIdentifier): Promise; + set( + identifier: ConfigurationIdentifier, + value: ConfigurationValue, + ): Promise; +} diff --git a/src/modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler.ts b/src/modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler.ts index f14b002..d71a463 100644 --- a/src/modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler.ts +++ b/src/modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler.ts @@ -2,8 +2,9 @@ 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'; +import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port'; +import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; @QueryHandler(GetConfigurationQuery) export class GetConfigurationQueryHandler implements IQueryHandler { @@ -12,6 +13,9 @@ export class GetConfigurationQueryHandler implements IQueryHandler { private readonly configurationRepository: ConfigurationRepositoryPort, ) {} async execute(query: GetConfigurationQuery): Promise { - return await this.configurationRepository.findOne(query); + return await this.configurationRepository.get({ + domain: query.domain as ConfigurationDomain, + key: query.key, + }); } } diff --git a/src/modules/configuration/core/application/services/populate.service.ts b/src/modules/configuration/core/application/services/populate.service.ts new file mode 100644 index 0000000..6f93db3 --- /dev/null +++ b/src/modules/configuration/core/application/services/populate.service.ts @@ -0,0 +1,62 @@ +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'; + +@Injectable() +export class PopulateService implements OnApplicationBootstrap { + constructor( + @Inject(CONFIGURATION_REPOSITORY) + private readonly configurationRepository: ConfigurationRepositoryPort, + private readonly configService: ConfigService, + ) {} + + onApplicationBootstrap() { + this._populate(); + } + + private _populate = async (): Promise => { + const carpoolConfig: CarpoolConfig = this.configService.get( + 'carpool', + ) as CarpoolConfig; + const paginationConfig: PaginationConfig = + this.configService.get( + 'pagination', + ) as PaginationConfig; + await Promise.all([ + this._populateConfig(ConfigurationDomain.CARPOOL, carpoolConfig), + this._populateConfig(ConfigurationDomain.PAGINATION, paginationConfig), + ]); + }; + + private _populateConfig = async ( + domain: ConfigurationDomain, + config: T, + ): Promise => { + let key: keyof typeof config; + for (key in config) { + try { + await this.configurationRepository.get({ + domain, + key, + }); + } catch (error: any) { + if (error instanceof NotFoundException) { + this.configurationRepository.set( + { + domain, + key, + }, + `${config[key]}`, + ); + } + } + } + }; +} diff --git a/src/modules/configuration/core/domain/configuration.default.ts b/src/modules/configuration/core/domain/configuration.default.ts deleted file mode 100644 index e933302..0000000 --- a/src/modules/configuration/core/domain/configuration.default.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - ConfigurationDomain, - ConfigurationType, - ConfigurationItems, -} from './configuration.types'; - -export const configurationItems: ConfigurationItems = { - [ConfigurationDomain.CARPOOL]: { - seatsProposed: { - value: '3', - type: ConfigurationType.NUMBER, - }, - seatsRequested: { - value: '1', - type: ConfigurationType.NUMBER, - }, - strictFrequency: { - value: 'false', - type: ConfigurationType.BOOLEAN, - }, - departureTimeMargin: { - value: '900', - type: ConfigurationType.NUMBER, - }, - role: { - value: 'passenger', - type: ConfigurationType.STRING, - enum: ['driver', 'passenger'], - }, - }, - [ConfigurationDomain.USER]: {}, -}; diff --git a/src/modules/configuration/core/domain/configuration.entity.ts b/src/modules/configuration/core/domain/configuration.entity.ts index 3167e78..cd67db8 100644 --- a/src/modules/configuration/core/domain/configuration.entity.ts +++ b/src/modules/configuration/core/domain/configuration.entity.ts @@ -5,8 +5,6 @@ import { 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 { protected readonly _id: AggregateID; @@ -15,36 +13,11 @@ export class ConfigurationEntity extends AggregateRoot { const id = v4(); const props: ConfigurationProps = { ...create }; const configuration = new ConfigurationEntity({ id, props }); - configuration.addEvent( - new ConfigurationSetDomainEvent({ - aggregateId: id, - identifier: props.identifier, - value: props.value, - type: props.type, - }), - ); return configuration; }; update(props: UpdateConfigurationProps): void { this.props.value = props.value; - this.addEvent( - new ConfigurationSetDomainEvent({ - aggregateId: this._id, - identifier: this.props.identifier, - value: props.value, - type: this.props.type, - }), - ); - } - - delete(): void { - this.addEvent( - new ConfigurationDeletedDomainEvent({ - aggregateId: this.id, - identifier: this.props.identifier, - }), - ); } validate(): void { diff --git a/src/modules/configuration/core/domain/configuration.types.ts b/src/modules/configuration/core/domain/configuration.types.ts index 28b2f42..889d9c2 100644 --- a/src/modules/configuration/core/domain/configuration.types.ts +++ b/src/modules/configuration/core/domain/configuration.types.ts @@ -2,14 +2,12 @@ export interface ConfigurationProps { identifier: ConfigurationIdentifier; value: ConfigurationValue; - type: ConfigurationType; } // Properties that are needed for a Configuration creation export interface CreateConfigurationProps { identifier: ConfigurationIdentifier; value: ConfigurationValue; - type: ConfigurationType; } export interface UpdateConfigurationProps { @@ -18,15 +16,10 @@ export interface UpdateConfigurationProps { export enum ConfigurationDomain { CARPOOL = 'CARPOOL', + PAGINATION = 'PAGINATION', USER = 'USER', } -export enum ConfigurationType { - BOOLEAN = 'BOOLEAN', - NUMBER = 'NUMBER', - STRING = 'STRING', -} - export type ConfigurationIdentifier = { domain: ConfigurationDomain; key: ConfigurationKey; @@ -35,14 +28,14 @@ export type ConfigurationIdentifier = { export type ConfigurationKey = string; export type ConfigurationValue = string; -export type ConfigurationItems = Record< - ConfigurationDomain, - Record< - ConfigurationKey, - { - value: ConfigurationValue; - type: ConfigurationType; - enum?: Array; - } - > ->; +export interface CarpoolConfig { + departureTimeMargin: number; + role: string; + seatsProposed: number; + seatsRequested: number; + strictFrequency: boolean; +} + +export interface PaginationConfig { + perPage: number; +} diff --git a/src/modules/configuration/core/domain/events/configuration-deleted.domain-event.ts b/src/modules/configuration/core/domain/events/configuration-deleted.domain-event.ts deleted file mode 100644 index 658e079..0000000 --- a/src/modules/configuration/core/domain/events/configuration-deleted.domain-event.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; -import { ConfigurationIdentifier } from '../configuration.types'; - -export class ConfigurationDeletedDomainEvent extends DomainEvent { - readonly identifier: ConfigurationIdentifier; - - constructor(props: DomainEventProps) { - super(props); - this.identifier = props.identifier; - } -} diff --git a/src/modules/configuration/core/domain/events/configuration-set.domain-event.ts b/src/modules/configuration/core/domain/events/configuration-set.domain-event.ts deleted file mode 100644 index c9b651e..0000000 --- a/src/modules/configuration/core/domain/events/configuration-set.domain-event.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; -import { - ConfigurationIdentifier, - ConfigurationType, - ConfigurationValue, -} from '../configuration.types'; - -export class ConfigurationSetDomainEvent extends DomainEvent { - readonly identifier: ConfigurationIdentifier; - readonly value: ConfigurationValue; - readonly type: ConfigurationType; - - constructor(props: DomainEventProps) { - super(props); - this.identifier = props.identifier; - this.type = props.type; - this.value = props.value; - } -} diff --git a/src/modules/configuration/infrastructure/configuration.repository.ts b/src/modules/configuration/infrastructure/configuration.repository.ts index 7284f81..8a29d5c 100644 --- a/src/modules/configuration/infrastructure/configuration.repository.ts +++ b/src/modules/configuration/infrastructure/configuration.repository.ts @@ -1,61 +1,47 @@ -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 { Injectable } from '@nestjs/common'; import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port'; -import { PrismaService } from './prisma.service'; -import { CONFIGURATION_MESSAGE_PUBLISHER } from '../configuration.di-tokens'; +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 ConfigurationBaseModel = { - uuid: string; - domain: string; +export type ConfigurationReadModel = { key: string; value: string; - type: string; }; +export type ConfigurationWriteModel = ConfigurationReadModel; -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 -{ +export class ConfigurationRepository 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, - }), - ); - } + @InjectRedis() private readonly redis: Redis, + private readonly mapper: ConfigurationMapper, + ) {} + + get = async ( + identifier: ConfigurationIdentifier, + ): Promise => { + 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 => { + await this.redis.set(`${identifier.domain}:${identifier.key}`, value); + }; } diff --git a/src/modules/configuration/infrastructure/prisma.service.ts b/src/modules/configuration/infrastructure/prisma.service.ts deleted file mode 100644 index 359f950..0000000 --- a/src/modules/configuration/infrastructure/prisma.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; - -@Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { - async onModuleInit() { - await this.$connect(); - } -} diff --git a/src/modules/configuration/interface/dtos/configuration.response.dto.ts b/src/modules/configuration/interface/dtos/configuration.response.dto.ts index f935c37..8b7a7b2 100644 --- a/src/modules/configuration/interface/dtos/configuration.response.dto.ts +++ b/src/modules/configuration/interface/dtos/configuration.response.dto.ts @@ -4,5 +4,4 @@ export class ConfigurationResponseDto extends ResponseBase { domain: string; key: string; value: string; - type: string; } diff --git a/src/modules/configuration/interface/grpc-controllers/configuration.proto b/src/modules/configuration/interface/grpc-controllers/configuration.proto index 6e55dc3..e479351 100644 --- a/src/modules/configuration/interface/grpc-controllers/configuration.proto +++ b/src/modules/configuration/interface/grpc-controllers/configuration.proto @@ -4,23 +4,18 @@ 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; + rpc Set(Configuration) returns (Empty); } + message ConfigurationByDomainKey { string domain = 1; string key = 2; } message Configuration { - string id = 1; - string domain = 2; - string key = 3; - string value = 4; + string domain = 1; + string key = 2; + string value = 3; } message Empty {} diff --git a/src/modules/configuration/interface/grpc-controllers/delete-configuration.grpc.controller.ts b/src/modules/configuration/interface/grpc-controllers/delete-configuration.grpc.controller.ts deleted file mode 100644 index 6a77383..0000000 --- a/src/modules/configuration/interface/grpc-controllers/delete-configuration.grpc.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -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 { - 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, - }); - } - } -} diff --git a/src/modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto.ts b/src/modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto.ts deleted file mode 100644 index 4930d76..0000000 --- a/src/modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; - -export class DeleteConfigurationRequestDto { - @IsEnum(ConfigurationDomain) - @IsNotEmpty() - domain: ConfigurationDomain; - - @IsString() - @IsNotEmpty() - key: string; -} diff --git a/src/modules/configuration/interface/grpc-controllers/propagate-configurations.grpc.controller.ts b/src/modules/configuration/interface/grpc-controllers/propagate-configurations.grpc.controller.ts deleted file mode 100644 index 6cbdc97..0000000 --- a/src/modules/configuration/interface/grpc-controllers/propagate-configurations.grpc.controller.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 { - try { - await this.commandBus.execute( - new PropagateConfigurationsCommand({ id: v4() }), - ); - } catch (error: any) { - throw new RpcException({ - code: RpcExceptionCode.UNKNOWN, - message: error.message, - }); - } - } -} diff --git a/src/modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller.ts b/src/modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller.ts index 1da64b0..3875c98 100644 --- a/src/modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller.ts +++ b/src/modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller.ts @@ -1,8 +1,6 @@ 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'; @@ -22,12 +20,11 @@ export class SetConfigurationGrpcController { @GrpcMethod(GRPC_SERVICE_NAME, 'Set') async set( setConfigurationRequestDto: SetConfigurationRequestDto, - ): Promise { + ): Promise { try { - const aggregateID: AggregateID = await this.commandBus.execute( + await this.commandBus.execute( new SetConfigurationCommand(setConfigurationRequestDto), ); - return new IdResponse(aggregateID); } catch (error: any) { throw new RpcException({ code: RpcExceptionCode.UNKNOWN, diff --git a/src/modules/configuration/tests/integration/configuration.repository.spec.ts b/src/modules/configuration/tests/integration/configuration.repository.spec.ts deleted file mode 100644 index 059b064..0000000 --- a/src/modules/configuration/tests/integration/configuration.repository.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - CONFIGURATION_MESSAGE_PUBLISHER, - CONFIGURATION_REPOSITORY, -} from '@modules/configuration/configuration.di-tokens'; -import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; -import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; -import { - CreateConfigurationProps, - ConfigurationDomain, -} 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('Configuration Repository', () => { - let prismaService: PrismaService; - let configurationRepository: ConfigurationRepository; - - const executeInsertCommand = async (table: string, object: any) => { - const command = `INSERT INTO "${table}" ("${Object.keys(object).join( - '","', - )}") VALUES ('${Object.values(object).join("','")}')`; - await prismaService.$executeRawUnsafe(command); - }; - const getSeed = (index: number, uuid: string): string => { - return `${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}`; - }; - - const baseUuid = { - uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00', - }; - - const createConfigurations = async (nbToCreate = 10) => { - for (let i = 0; i < nbToCreate; i++) { - const configurationToCreate = { - uuid: getSeed(i, baseUuid.uuid), - domain: ConfigurationDomain.AD, - key: `key${i}`, - value: `value${i}`, - createdAt: '2023-07-24 13:07:05.000', - updatedAt: '2023-07-24 13:07:05.000', - }; - configurationToCreate.uuid = getSeed(i, baseUuid.uuid); - await executeInsertCommand('configuration', configurationToCreate); - } - }; - - const mockMessagePublisher = { - publish: jest.fn().mockImplementation(), - }; - - const mockLogger = { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - - beforeAll(async () => { - const module = await Test.createTestingModule({ - imports: [ - EventEmitterModule.forRoot(), - ConfigModule.forRoot({ isGlobal: true }), - ], - providers: [ - PrismaService, - ConfigurationMapper, - { - provide: CONFIGURATION_REPOSITORY, - useClass: ConfigurationRepository, - }, - { - provide: CONFIGURATION_MESSAGE_PUBLISHER, - useValue: mockMessagePublisher, - }, - ], - }) - // disable logging - .setLogger(mockLogger) - .compile(); - - prismaService = module.get(PrismaService); - configurationRepository = module.get( - CONFIGURATION_REPOSITORY, - ); - }); - - afterAll(async () => { - await prismaService.$disconnect(); - }); - - beforeEach(async () => { - await prismaService.configuration.deleteMany(); - }); - - describe('findOne', () => { - it('should return a configuration', async () => { - await createConfigurations(1); - const result = await configurationRepository.findOne({ - domain: ConfigurationDomain.AD, - key: 'key0', - }); - expect(result.getProps().value).toBe('value0'); - }); - }); - - describe('findAll', () => { - it('should return all configurations', async () => { - await createConfigurations(10); - const configurations: ConfigurationEntity[] = - await configurationRepository.findAll({}); - expect(configurations).toHaveLength(10); - }); - }); - - describe('create', () => { - it('should create a configuration', async () => { - const beforeCount = await prismaService.configuration.count(); - - const createConfigurationProps: CreateConfigurationProps = { - domain: ConfigurationDomain.AD, - key: 'seatsProposed', - value: '3', - }; - - const configurationToCreate: ConfigurationEntity = - ConfigurationEntity.create(createConfigurationProps); - await configurationRepository.insert(configurationToCreate); - - const afterCount = await prismaService.configuration.count(); - - expect(afterCount - beforeCount).toBe(1); - }); - }); - - describe('update', () => { - it('should update a configuration', async () => { - await createConfigurations(1); - const configurationToUpdate: ConfigurationEntity = - await configurationRepository.findOne({ - domain: ConfigurationDomain.AD, - key: 'key0', - }); - configurationToUpdate.update({ value: 'newValue' }); - await configurationRepository.update( - configurationToUpdate.id, - configurationToUpdate, - ); - const result: ConfigurationEntity = await configurationRepository.findOne( - { - domain: ConfigurationDomain.AD, - key: 'key0', - }, - ); - expect(result.getProps().value).toBe('newValue'); - }); - }); - - describe('delete', () => { - it('should delete a configuration', async () => { - await createConfigurations(10); - const beforeCount = await prismaService.configuration.count(); - const configurationToDelete: ConfigurationEntity = - await configurationRepository.findOne({ - domain: ConfigurationDomain.AD, - key: 'key4', - }); - await configurationRepository.delete(configurationToDelete); - const afterCount = await prismaService.configuration.count(); - expect(afterCount - beforeCount).toBe(-1); - }); - }); -}); diff --git a/src/modules/configuration/tests/unit/configuration.mapper.spec.ts b/src/modules/configuration/tests/unit/configuration.mapper.spec.ts index 2686aaa..2345033 100644 --- a/src/modules/configuration/tests/unit/configuration.mapper.spec.ts +++ b/src/modules/configuration/tests/unit/configuration.mapper.spec.ts @@ -1,9 +1,6 @@ import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; -import { - ConfigurationDomain, - ConfigurationType, -} from '@modules/configuration/core/domain/configuration.types'; +import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types'; import { ConfigurationReadModel, ConfigurationWriteModel, @@ -20,19 +17,13 @@ const configurationEntity: ConfigurationEntity = new ConfigurationEntity({ key: 'seatsProposed', }, value: '3', - type: ConfigurationType.NUMBER, }, createdAt: now, updatedAt: now, }); const configurationReadModel: ConfigurationReadModel = { - uuid: 'c160cf8c-f057-4962-841f-3ad68346df44', - domain: 'AD', - key: 'seatsProposed', + key: 'AD:seatsProposed', value: '4', - type: 'NUMBER', - createdAt: now, - updatedAt: now, }; describe('Configuration Mapper', () => { diff --git a/src/modules/configuration/tests/unit/core/configuration.entity.spec.ts b/src/modules/configuration/tests/unit/core/configuration.entity.spec.ts index 0647071..96a817e 100644 --- a/src/modules/configuration/tests/unit/core/configuration.entity.spec.ts +++ b/src/modules/configuration/tests/unit/core/configuration.entity.spec.ts @@ -3,10 +3,7 @@ import { CreateConfigurationProps, ConfigurationDomain, UpdateConfigurationProps, - ConfigurationType, } 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 = { identifier: { @@ -14,7 +11,6 @@ const createConfigurationProps: CreateConfigurationProps = { key: 'seatsProposed', }, value: '3', - type: ConfigurationType.NUMBER, }; const updateConfigurationProps: UpdateConfigurationProps = { @@ -28,10 +24,6 @@ describe('Configuration entity create', () => { ); expect(configurationEntity.id.length).toBe(36); expect(configurationEntity.getProps().value).toBe('3'); - expect(configurationEntity.domainEvents.length).toBe(1); - expect(configurationEntity.domainEvents[0]).toBeInstanceOf( - ConfigurationSetDomainEvent, - ); }); }); @@ -42,24 +34,5 @@ describe('Configuration entity update', () => { ); 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, - ); }); }); diff --git a/src/modules/configuration/tests/unit/core/delete-configuration.service.spec.ts b/src/modules/configuration/tests/unit/core/delete-configuration.service.spec.ts deleted file mode 100644 index 4236ba2..0000000 --- a/src/modules/configuration/tests/unit/core/delete-configuration.service.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -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 { ConfigurationDomain } 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: ConfigurationDomain.CARPOOL, - 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, - ); - }); - - 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(); - }); - }); -}); diff --git a/src/modules/configuration/tests/unit/core/get-configuration.query-handler.spec.ts b/src/modules/configuration/tests/unit/core/get-configuration.query-handler.spec.ts index dd2384f..4171af4 100644 --- a/src/modules/configuration/tests/unit/core/get-configuration.query-handler.spec.ts +++ b/src/modules/configuration/tests/unit/core/get-configuration.query-handler.spec.ts @@ -1,9 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity'; -import { - ConfigurationDomain, - ConfigurationType, -} from '@modules/configuration/core/domain/configuration.types'; +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'; @@ -17,14 +14,13 @@ const configuration: ConfigurationEntity = new ConfigurationEntity({ key: 'seatsProposed', }, value: '3', - type: ConfigurationType.NUMBER, }, createdAt: now, updatedAt: now, }); const mockConfigurationRepository = { - findOne: jest.fn().mockImplementation(() => configuration), + get: jest.fn().mockImplementation(() => configuration), }; describe('Get Configuration Query Handler', () => { diff --git a/src/modules/configuration/tests/unit/core/populate.service.spec.ts b/src/modules/configuration/tests/unit/core/populate.service.spec.ts new file mode 100644 index 0000000..b1d6923 --- /dev/null +++ b/src/modules/configuration/tests/unit/core/populate.service.spec.ts @@ -0,0 +1,84 @@ +import { NotFoundException } from '@mobicoop/ddd-library'; +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(() => { + throw new NotFoundException('Configuration not found'); + }), + set: jest.fn(), +}; + +const mockConfigService = { + get: jest.fn().mockImplementation((domain: string) => { + switch (domain) { + case 'carpool': + return { + departureTimeMargin: 900, + role: 'passenger', + seatsProposed: 3, + seatsRequested: 1, + strictFrequency: false, + }; + case 'pagination': + return { + perPage: 10, + }; + } + }), +}; + +describe('Populate Service', () => { + let populateService: PopulateService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CONFIGURATION_REPOSITORY, + useValue: mockConfigurationRepository, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + PopulateService, + ], + }).compile(); + + populateService = module.get(PopulateService); + }); + + it('should be defined', () => { + expect(populateService).toBeDefined(); + }); + + it('should populate database with default values', () => { + jest.spyOn(mockConfigurationRepository, 'get'); + jest.spyOn(mockConfigurationRepository, 'set'); + populateService.onApplicationBootstrap(); + expect(mockConfigurationRepository.get).toHaveBeenCalled(); + expect(mockConfigurationRepository.set).toHaveBeenCalled(); + }); +}); diff --git a/src/modules/configuration/tests/unit/core/propagate-configurations.service.spec.ts b/src/modules/configuration/tests/unit/core/propagate-configurations.service.spec.ts deleted file mode 100644 index 16d1442..0000000 --- a/src/modules/configuration/tests/unit/core/propagate-configurations.service.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { - ConfigurationDomain, - ConfigurationType, -} 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: { - identifier: { - domain: ConfigurationDomain.CARPOOL, - key: 'seatsProposed', - }, - value: '3', - type: ConfigurationType.NUMBER, - }, - 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: { - identifier: { - domain: ConfigurationDomain.CARPOOL, - key: 'seatsRequested', - }, - value: '1', - type: ConfigurationType.NUMBER, - }, - createdAt: new Date('2023-10-23T07:00:00Z'), - updatedAt: new Date('2023-10-23T07:00:00Z'), - }), -]; - -const mockConfigurationMapper = { - toResponse: jest - .fn() - .mockImplementationOnce(() => ({ - domain: ConfigurationDomain.CARPOOL, - key: 'seatsProposed', - value: '3', - type: ConfigurationType.NUMBER, - })) - .mockImplementationOnce(() => ({ - domain: ConfigurationDomain.CARPOOL, - key: 'seatsRequested', - value: '1', - type: ConfigurationType.NUMBER, - })), -}; - -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, - ); - }); - - 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":"CARPOOL","key":"seatsProposed","value":"3","type":"NUMBER"},{"domain":"CARPOOL","key":"seatsRequested","value":"1","type":"NUMBER"}]', - ); - }); - }); -}); diff --git a/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-deleted.domain-event-handler.spec.ts b/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-deleted.domain-event-handler.spec.ts deleted file mode 100644 index 46f350c..0000000 --- a/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-deleted.domain-event-handler.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -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 { ConfigurationDomain } 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, - ); - }); - - it('should publish a message', () => { - jest.spyOn(mockMessagePublisher, 'publish'); - const configurationDeletedDomainEvent: ConfigurationDeletedDomainEvent = { - id: 'some-domain-event-id', - identifier: { - domain: ConfigurationDomain.CARPOOL, - 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":"CARPOOL","key":"seatsProposed"}', - ); - }); -}); diff --git a/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-set.domain-event-handler.spec.ts b/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-set.domain-event-handler.spec.ts deleted file mode 100644 index e6cb4c2..0000000 --- a/src/modules/configuration/tests/unit/core/publish-message-when-configuration-is-set.domain-event-handler.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 { - ConfigurationDomain, - ConfigurationType, -} 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, - ); - }); - - it('should publish a message', () => { - jest.spyOn(mockMessagePublisher, 'publish'); - const configurationSetDomainEvent: ConfigurationSetDomainEvent = { - id: 'some-domain-event-id', - aggregateId: 'some-aggregate-id', - identifier: { - domain: ConfigurationDomain.CARPOOL, - key: 'seatsProposed', - }, - type: ConfigurationType.NUMBER, - 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":"CARPOOL","key":"seatsProposed","value":"3"}', - ); - }); -}); diff --git a/src/modules/configuration/tests/unit/core/set-configuration.service.spec.ts b/src/modules/configuration/tests/unit/core/set-configuration.service.spec.ts index d4b4f38..35aec9b 100644 --- a/src/modules/configuration/tests/unit/core/set-configuration.service.spec.ts +++ b/src/modules/configuration/tests/unit/core/set-configuration.service.spec.ts @@ -1,10 +1,6 @@ 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 { - ConfigurationDomain, - ConfigurationType, -} from '@modules/configuration/core/domain/configuration.types'; +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'; @@ -16,38 +12,8 @@ const setConfigurationRequest: SetConfigurationRequestDto = { value: '3', }; -const existingConfigurationEntity = new ConfigurationEntity({ - id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', - props: { - identifier: { - domain: ConfigurationDomain.CARPOOL, - key: 'seatsProposed', - }, - type: ConfigurationType.NUMBER, - 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 + set: jest .fn() .mockImplementationOnce(() => ({})) .mockImplementationOnce(() => { @@ -82,31 +48,13 @@ describe('Set Configuration Service', () => { const setConfigurationCommand = new SetConfigurationCommand( setConfigurationRequest, ); - it('should create a new configuration item', async () => { + it('should set an existing configuration item', async () => { + jest.spyOn(mockConfigurationRepository, 'set'); 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'); + 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({ diff --git a/src/modules/configuration/tests/unit/infrastructure/configuration.repository.spec.ts b/src/modules/configuration/tests/unit/infrastructure/configuration.repository.spec.ts index 0e6de77..319b446 100644 --- a/src/modules/configuration/tests/unit/infrastructure/configuration.repository.spec.ts +++ b/src/modules/configuration/tests/unit/infrastructure/configuration.repository.spec.ts @@ -1,36 +1,93 @@ +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 { PrismaService } from '@modules/configuration/infrastructure/prisma.service'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; import { Test, TestingModule } from '@nestjs/testing'; +import { getRedisToken } from '@songkeys/nestjs-redis'; -const mockMessagePublisher = { - publish: jest.fn().mockImplementation(), +const mockRedis = { + get: jest + .fn() + .mockImplementationOnce(() => '1') + .mockImplementation(() => null), + set: jest.fn().mockImplementation(), }; -describe('Configuration repository', () => { - let prismaService: PrismaService; - let configurationMapper: ConfigurationMapper; - let eventEmitter: EventEmitter2; +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({ - imports: [EventEmitterModule.forRoot()], - providers: [PrismaService, ConfigurationMapper], + providers: [ + { + provide: getRedisToken('default'), + useValue: mockRedis, + }, + { + provide: ConfigurationMapper, + useValue: mockConfigurationMapper, + }, + ConfigurationRepository, + ], }).compile(); - prismaService = module.get(PrismaService); - configurationMapper = module.get(ConfigurationMapper); - eventEmitter = module.get(EventEmitter2); + configurationRepository = module.get( + ConfigurationRepository, + ); }); + it('should be defined', () => { - expect( - new ConfigurationRepository( - prismaService, - configurationMapper, - eventEmitter, - mockMessagePublisher, - ), - ).toBeDefined(); + 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(); + }); }); }); diff --git a/src/modules/configuration/tests/unit/interface/delete-configuration.grpc.controller.spec.ts b/src/modules/configuration/tests/unit/interface/delete-configuration.grpc.controller.spec.ts deleted file mode 100644 index 980a267..0000000 --- a/src/modules/configuration/tests/unit/interface/delete-configuration.grpc.controller.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - DatabaseErrorException, - NotFoundException, -} from '@mobicoop/ddd-library'; -import { RpcExceptionCode } from '@mobicoop/ddd-library'; -import { ConfigurationDomain } 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: ConfigurationDomain.CARPOOL, - 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, - ); - }); - - 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); - }); -}); diff --git a/src/modules/configuration/tests/unit/interface/get-configuration.grpc.controller.spec.ts b/src/modules/configuration/tests/unit/interface/get-configuration.grpc.controller.spec.ts index 604005f..97b7d36 100644 --- a/src/modules/configuration/tests/unit/interface/get-configuration.grpc.controller.spec.ts +++ b/src/modules/configuration/tests/unit/interface/get-configuration.grpc.controller.spec.ts @@ -1,10 +1,7 @@ import { NotFoundException } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { ConfigurationMapper } from '@modules/configuration/configuration.mapper'; -import { - ConfigurationDomain, - ConfigurationType, -} from '@modules/configuration/core/domain/configuration.types'; +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'; @@ -27,7 +24,6 @@ const mockConfigurationMapper = { domain: ConfigurationDomain.CARPOOL, key: 'seatsProposed', value: '3', - type: ConfigurationType.NUMBER, })), }; diff --git a/src/modules/configuration/tests/unit/interface/propagate-configurations.grpc.controller.spec.ts b/src/modules/configuration/tests/unit/interface/propagate-configurations.grpc.controller.spec.ts deleted file mode 100644 index 9a74622..0000000 --- a/src/modules/configuration/tests/unit/interface/propagate-configurations.grpc.controller.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -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, - ); - }); - - 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); - }); -}); diff --git a/src/modules/configuration/tests/unit/interface/set-configuration.grpc.controller.spec.ts b/src/modules/configuration/tests/unit/interface/set-configuration.grpc.controller.spec.ts index 8a9d5f4..94e78f0 100644 --- a/src/modules/configuration/tests/unit/interface/set-configuration.grpc.controller.spec.ts +++ b/src/modules/configuration/tests/unit/interface/set-configuration.grpc.controller.spec.ts @@ -1,4 +1,3 @@ -import { IdResponse } 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'; @@ -51,11 +50,7 @@ describe('Set Configuration Grpc Controller', () => { 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'); + await setConfigurationGrpcController.set(setConfigurationRequest); expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); });