diff --git a/.env.dist b/.env.dist index 63d7ff5..32d1b7a 100644 --- a/.env.dist +++ b/.env.dist @@ -1,6 +1,7 @@ # SERVICE SERVICE_URL=0.0.0.0 SERVICE_PORT=5001 +SERVICE_CONFIGURATION_DOMAIN=USER # PRISMA DATABASE_URL="postgresql://user:user@v3-user-db:5432/user?schema=public" @@ -10,3 +11,8 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# REDIS +REDIS_IMAGE=redis/redis-stack:latest +REDIS_HOST=v3-user-redis +REDIS_PORT=6379 diff --git a/.env.test b/.env.test index f3790b9..4169556 100644 --- a/.env.test +++ b/.env.test @@ -1,6 +1,7 @@ # SERVICE SERVICE_URL=0.0.0.0 SERVICE_PORT=5001 +SERVICE_CONFIGURATION_DOMAIN=USER # PRISMA DATABASE_URL="postgresql://user:user@localhost:5601/user?schema=public" @@ -10,3 +11,8 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# REDIS +REDIS_IMAGE=redis/redis-stack:latest +REDIS_HOST=v3-user-redis +REDIS_PORT=6379 diff --git a/docker-compose.yml b/docker-compose.yml index db9e43d..9ade76b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,19 @@ services: aliases: - v3-user-api + redis: + container_name: v3-user-redis + image: ${REDIS_IMAGE} + ports: + - 6379:6379 + - 8101:8001 + env_file: + - .env + networks: + v3-network: + aliases: + - v3-user-redis + db: container_name: v3-user-db image: ${POSTGRES_IMAGE} diff --git a/package-lock.json b/package-lock.json index 2fa8544..7fdf49c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "mobicoop-v3-user", + "name": "@mobicoop/user", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "mobicoop-v3-user", + "name": "@mobicoop/user", "version": "0.0.1", "license": "AGPL", "dependencies": { @@ -15,6 +15,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -26,7 +27,8 @@ "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "class-transformer": "^0.5.1", - "dotenv-cli": "^6.0.0" + "dotenv-cli": "^6.0.0", + "ioredis": "^5.3.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -1104,6 +1106,11 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "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/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1687,6 +1694,22 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@liaoliaots/nestjs-redis": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@liaoliaots/nestjs-redis/-/nestjs-redis-9.0.5.tgz", + "integrity": "sha512-nPcGLj0zW4mEsYtQYfWx3o7PmrMjuzFk6+t/g2IRopAeWWUZZ/5nIJ4KTKiz/3DJEUkbX8PZqB+dOhklGF0SVA==", + "dependencies": { + "tslib": "2.4.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "ioredis": "^5.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.1.8", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.8.tgz", @@ -3719,6 +3742,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", @@ -3841,9 +3872,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "node_modules/core-util-is": { @@ -3955,6 +3986,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", @@ -5259,6 +5298,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", + "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", + "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", @@ -6427,6 +6489,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", @@ -7441,6 +7513,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", @@ -7897,6 +7988,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", @@ -9779,6 +9875,11 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10234,6 +10335,14 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@liaoliaots/nestjs-redis": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@liaoliaots/nestjs-redis/-/nestjs-redis-9.0.5.tgz", + "integrity": "sha512-nPcGLj0zW4mEsYtQYfWx3o7PmrMjuzFk6+t/g2IRopAeWWUZZ/5nIJ4KTKiz/3DJEUkbX8PZqB+dOhklGF0SVA==", + "requires": { + "tslib": "2.4.1" + } + }, "@nestjs/cli": { "version": "9.1.8", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.8.tgz", @@ -11740,6 +11849,11 @@ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true }, + "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==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11837,9 +11951,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "core-util-is": { @@ -11925,6 +12039,11 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -12889,6 +13008,22 @@ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, + "ioredis": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", + "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", + "requires": { + "@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" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -13770,6 +13905,16 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "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==" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14509,6 +14654,19 @@ "resolve": "^1.1.6" } }, + "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==" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "requires": { + "redis-errors": "^1.0.0" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -14847,6 +15005,11 @@ } } }, + "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==" + }, "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 a30c439..5712de9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:e2e": "jest --config ./test/jest-e2e.json", + "generate": "docker exec v3-user sh -c 'npx prisma generate'", "migrate": "docker exec v3-user sh -c 'npx prisma migrate dev'", "migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy", "migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy" @@ -32,6 +33,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -43,7 +45,8 @@ "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "class-transformer": "^0.5.1", - "dotenv-cli": "^6.0.0" + "dotenv-cli": "^6.0.0", + "ioredis": "^5.3.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index b919317..ce77035 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ConfigurationModule } from './modules/configuration/configuration.module'; import { UsersModule } from './modules/users/users.module'; @Module({ @@ -9,6 +10,7 @@ import { UsersModule } from './modules/users/users.module'; ConfigModule.forRoot({ isGlobal: true }), AutomapperModule.forRoot({ strategyInitializer: classes() }), UsersModule, + ConfigurationModule, ], controllers: [], providers: [], diff --git a/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts new file mode 100644 index 0000000..3a7cd0f --- /dev/null +++ b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts @@ -0,0 +1,58 @@ +import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; +import { Controller } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CommandBus } from '@nestjs/cqrs'; +import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; +import { SetConfigurationCommand } from '../../commands/set-configuration.command'; +import { DeleteConfigurationRequest } from '../../domain/dtos/delete-configuration.request'; +import { SetConfigurationRequest } from '../../domain/dtos/set-configuration.request'; + +@Controller() +export class ConfigurationMessagerController { + constructor( + private readonly _commandBus: CommandBus, + private readonly _configService: ConfigService, + ) {} + + @RabbitSubscribe({ + exchange: 'configuration', + routingKey: ['create', 'update'], + queue: 'configuration-update', + }) + public async setConfigurationHandler(message: string) { + const configuration = JSON.parse(message); + if ( + configuration.domain == + this._configService.get('SERVICE_CONFIGURATION_DOMAIN') + ) { + const setConfigurationRequest: SetConfigurationRequest = + new SetConfigurationRequest(); + setConfigurationRequest.domain = configuration.domain; + setConfigurationRequest.key = configuration.key; + setConfigurationRequest.value = configuration.value; + await this._commandBus.execute( + new SetConfigurationCommand(setConfigurationRequest), + ); + } + } + + @RabbitSubscribe({ + exchange: 'configuration', + routingKey: 'delete', + queue: 'configuration-delete', + }) + public async configurationDeletedHandler(message: string) { + const deletedConfiguration = JSON.parse(message); + if ( + deletedConfiguration.domain == + this._configService.get('SERVICE_CONFIGURATION_DOMAIN') + ) { + const deleteConfigurationRequest = new DeleteConfigurationRequest(); + deleteConfigurationRequest.domain = deletedConfiguration.domain; + deleteConfigurationRequest.key = deletedConfiguration.key; + await this._commandBus.execute( + new DeleteConfigurationCommand(deleteConfigurationRequest), + ); + } + } +} diff --git a/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts b/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts new file mode 100644 index 0000000..2de14f0 --- /dev/null +++ b/src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts @@ -0,0 +1,23 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import { IConfigurationRepository } from '../../domain/interfaces/configuration.repository'; + +@Injectable() +export class RedisConfigurationRepository extends IConfigurationRepository { + constructor(@InjectRedis() private readonly _redis: Redis) { + super(); + } + + async get(key: string): Promise { + return await this._redis.get(key); + } + + async set(key: string, value: string) { + await this._redis.set(key, value); + } + + async del(key: string) { + await this._redis.del(key); + } +} diff --git a/src/modules/configuration/commands/delete-configuration.command.ts b/src/modules/configuration/commands/delete-configuration.command.ts new file mode 100644 index 0000000..8a6753e --- /dev/null +++ b/src/modules/configuration/commands/delete-configuration.command.ts @@ -0,0 +1,9 @@ +import { DeleteConfigurationRequest } from '../domain/dtos/delete-configuration.request'; + +export class DeleteConfigurationCommand { + readonly deleteConfigurationRequest: DeleteConfigurationRequest; + + constructor(deleteConfigurationRequest: DeleteConfigurationRequest) { + this.deleteConfigurationRequest = deleteConfigurationRequest; + } +} diff --git a/src/modules/configuration/commands/set-configuration.command.ts b/src/modules/configuration/commands/set-configuration.command.ts new file mode 100644 index 0000000..52f54ee --- /dev/null +++ b/src/modules/configuration/commands/set-configuration.command.ts @@ -0,0 +1,9 @@ +import { SetConfigurationRequest } from '../domain/dtos/set-configuration.request'; + +export class SetConfigurationCommand { + readonly setConfigurationRequest: SetConfigurationRequest; + + constructor(setConfigurationRequest: SetConfigurationRequest) { + this.setConfigurationRequest = setConfigurationRequest; + } +} diff --git a/src/modules/configuration/configuration.module.ts b/src/modules/configuration/configuration.module.ts new file mode 100644 index 0000000..1c3cdff --- /dev/null +++ b/src/modules/configuration/configuration.module.ts @@ -0,0 +1,53 @@ +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { ConfigurationMessagerController } from './adapters/primaries/configuration-messager.controller'; +import { RedisConfigurationRepository } from './adapters/secondaries/redis-configuration.repository'; +import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase'; +import { GetConfigurationUseCase } from './domain/usecases/get-configuration.usecase'; +import { SetConfigurationUseCase } from './domain/usecases/set-configuration.usecase'; + +@Module({ + imports: [ + CqrsModule, + RedisModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async ( + configService: ConfigService, + ): Promise => { + return { + config: { + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + }, + }; + }, + }), + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + exchanges: [ + { + name: 'configuration', + type: 'topic', + }, + ], + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + enableControllerDiscovery: true, + }), + inject: [ConfigService], + }), + ], + controllers: [ConfigurationMessagerController], + providers: [ + GetConfigurationUseCase, + SetConfigurationUseCase, + DeleteConfigurationUseCase, + RedisConfigurationRepository, + ], +}) +export class ConfigurationModule {} diff --git a/src/modules/configuration/domain/dtos/delete-configuration.request.ts b/src/modules/configuration/domain/dtos/delete-configuration.request.ts new file mode 100644 index 0000000..3430832 --- /dev/null +++ b/src/modules/configuration/domain/dtos/delete-configuration.request.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteConfigurationRequest { + @IsString() + @IsNotEmpty() + domain: string; + + @IsString() + @IsNotEmpty() + key: string; +} diff --git a/src/modules/configuration/domain/dtos/set-configuration.request.ts b/src/modules/configuration/domain/dtos/set-configuration.request.ts new file mode 100644 index 0000000..3ed3fff --- /dev/null +++ b/src/modules/configuration/domain/dtos/set-configuration.request.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SetConfigurationRequest { + @IsString() + @IsNotEmpty() + domain: string; + + @IsString() + @IsNotEmpty() + key: string; + + @IsString() + @IsNotEmpty() + value: string; +} diff --git a/src/modules/configuration/domain/entities/configuration.ts b/src/modules/configuration/domain/entities/configuration.ts new file mode 100644 index 0000000..8d3d6c5 --- /dev/null +++ b/src/modules/configuration/domain/entities/configuration.ts @@ -0,0 +1,15 @@ +import { AutoMap } from '@automapper/classes'; + +export class Configuration { + @AutoMap() + uuid: string; + + @AutoMap() + domain: string; + + @AutoMap() + key: string; + + @AutoMap() + value: string; +} diff --git a/src/modules/configuration/domain/interfaces/configuration.repository.ts b/src/modules/configuration/domain/interfaces/configuration.repository.ts new file mode 100644 index 0000000..657e5fd --- /dev/null +++ b/src/modules/configuration/domain/interfaces/configuration.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export abstract class IConfigurationRepository { + abstract get(key: string): Promise; + abstract set(key: string, value: string): void; + abstract del(key: string): void; +} diff --git a/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts new file mode 100644 index 0000000..14ab3cb --- /dev/null +++ b/src/modules/configuration/domain/usecases/delete-configuration.usecase.ts @@ -0,0 +1,16 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; + +@CommandHandler(DeleteConfigurationCommand) +export class DeleteConfigurationUseCase { + constructor(private _configurationRepository: RedisConfigurationRepository) {} + + async execute(deleteConfigurationCommand: DeleteConfigurationCommand) { + await this._configurationRepository.del( + deleteConfigurationCommand.deleteConfigurationRequest.domain + + ':' + + deleteConfigurationCommand.deleteConfigurationRequest.key, + ); + } +} diff --git a/src/modules/configuration/domain/usecases/get-configuration.usecase.ts b/src/modules/configuration/domain/usecases/get-configuration.usecase.ts new file mode 100644 index 0000000..38036ff --- /dev/null +++ b/src/modules/configuration/domain/usecases/get-configuration.usecase.ts @@ -0,0 +1,14 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { GetConfigurationQuery } from '../../queries/get-configuration.query'; + +@QueryHandler(GetConfigurationQuery) +export class GetConfigurationUseCase { + constructor(private _configurationRepository: RedisConfigurationRepository) {} + + async execute(getConfigurationQuery: GetConfigurationQuery): Promise { + return this._configurationRepository.get( + getConfigurationQuery.domain + ':' + getConfigurationQuery.key, + ); + } +} diff --git a/src/modules/configuration/domain/usecases/set-configuration.usecase.ts b/src/modules/configuration/domain/usecases/set-configuration.usecase.ts new file mode 100644 index 0000000..408340a --- /dev/null +++ b/src/modules/configuration/domain/usecases/set-configuration.usecase.ts @@ -0,0 +1,17 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { SetConfigurationCommand } from '../../commands/set-configuration.command'; + +@CommandHandler(SetConfigurationCommand) +export class SetConfigurationUseCase { + constructor(private _configurationRepository: RedisConfigurationRepository) {} + + async execute(setConfigurationCommand: SetConfigurationCommand) { + await this._configurationRepository.set( + setConfigurationCommand.setConfigurationRequest.domain + + ':' + + setConfigurationCommand.setConfigurationRequest.key, + setConfigurationCommand.setConfigurationRequest.value, + ); + } +} diff --git a/src/modules/configuration/queries/get-configuration.query.ts b/src/modules/configuration/queries/get-configuration.query.ts new file mode 100644 index 0000000..62211e7 --- /dev/null +++ b/src/modules/configuration/queries/get-configuration.query.ts @@ -0,0 +1,9 @@ +export class GetConfigurationQuery { + readonly domain: string; + readonly key: string; + + constructor(domain: string, key: string) { + this.domain = domain; + this.key = key; + } +} diff --git a/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts b/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts new file mode 100644 index 0000000..a28ae3c --- /dev/null +++ b/src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts @@ -0,0 +1,49 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; +import { DeleteConfigurationRequest } from '../../domain/dtos/delete-configuration.request'; +import { DeleteConfigurationUseCase } from '../../domain/usecases/delete-configuration.usecase'; + +const mockRedisConfigurationRepository = { + del: jest.fn().mockResolvedValue(undefined), +}; + +describe('DeleteConfigurationUseCase', () => { + let deleteConfigurationUseCase: DeleteConfigurationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: RedisConfigurationRepository, + useValue: mockRedisConfigurationRepository, + }, + + DeleteConfigurationUseCase, + ], + }).compile(); + + deleteConfigurationUseCase = module.get( + DeleteConfigurationUseCase, + ); + }); + + it('should be defined', () => { + expect(deleteConfigurationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should delete a key', async () => { + jest.spyOn(mockRedisConfigurationRepository, 'del'); + const deleteConfigurationRequest: DeleteConfigurationRequest = { + domain: 'my-domain', + key: 'my-key', + }; + await deleteConfigurationUseCase.execute( + new DeleteConfigurationCommand(deleteConfigurationRequest), + ); + + expect(mockRedisConfigurationRepository.del).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/configuration/tests/unit/get-configuration.usecase.spec.ts b/src/modules/configuration/tests/unit/get-configuration.usecase.spec.ts new file mode 100644 index 0000000..a94fc70 --- /dev/null +++ b/src/modules/configuration/tests/unit/get-configuration.usecase.spec.ts @@ -0,0 +1,43 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { GetConfigurationUseCase } from '../../domain/usecases/get-configuration.usecase'; +import { GetConfigurationQuery } from '../../queries/get-configuration.query'; + +const mockRedisConfigurationRepository = { + get: jest.fn().mockResolvedValue('my-value'), +}; + +describe('GetConfigurationUseCase', () => { + let getConfigurationUseCase: GetConfigurationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: RedisConfigurationRepository, + useValue: mockRedisConfigurationRepository, + }, + + GetConfigurationUseCase, + ], + }).compile(); + + getConfigurationUseCase = module.get( + GetConfigurationUseCase, + ); + }); + + it('should be defined', () => { + expect(getConfigurationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should get a value for a key', async () => { + const value: string = await getConfigurationUseCase.execute( + new GetConfigurationQuery('my-domain', 'my-key'), + ); + + expect(value).toBe('my-value'); + }); + }); +}); diff --git a/src/modules/configuration/tests/unit/set-configuration.usecase.spec.ts b/src/modules/configuration/tests/unit/set-configuration.usecase.spec.ts new file mode 100644 index 0000000..f6e25d1 --- /dev/null +++ b/src/modules/configuration/tests/unit/set-configuration.usecase.spec.ts @@ -0,0 +1,50 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { SetConfigurationCommand } from '../../commands/set-configuration.command'; +import { SetConfigurationRequest } from '../../domain/dtos/set-configuration.request'; +import { SetConfigurationUseCase } from '../../domain/usecases/set-configuration.usecase'; + +const mockRedisConfigurationRepository = { + set: jest.fn().mockResolvedValue(undefined), +}; + +describe('SetConfigurationUseCase', () => { + let setConfigurationUseCase: SetConfigurationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: RedisConfigurationRepository, + useValue: mockRedisConfigurationRepository, + }, + + SetConfigurationUseCase, + ], + }).compile(); + + setConfigurationUseCase = module.get( + SetConfigurationUseCase, + ); + }); + + it('should be defined', () => { + expect(setConfigurationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should set a value for a key', async () => { + jest.spyOn(mockRedisConfigurationRepository, 'set'); + const setConfigurationRequest: SetConfigurationRequest = { + domain: 'my-domain', + key: 'my-key', + value: 'my-value', + }; + await setConfigurationUseCase.execute( + new SetConfigurationCommand(setConfigurationRequest), + ); + + expect(mockRedisConfigurationRepository.set).toHaveBeenCalledTimes(1); + }); + }); +});