From 706ea81b9f702ee13c68766cbeea70e8af7c617c Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 3 May 2023 17:31:26 +0200 Subject: [PATCH] install dependencies, create basic models --- .env.dist | 17 + Dockerfile | 77 ++ docker-compose.yml | 26 + package-lock.json | 835 +++++++++++++++++- package.json | 54 +- .../20230503152948_init/migration.sql | 60 ++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 62 ++ src/app.module.ts | 11 +- src/main.ts | 22 +- src/modules/ad/ad.module.ts | 8 + .../ad/adapters/secondaries/ads.repository.ts | 8 + src/modules/ad/domain/entities/ad.ts | 6 + .../configuration-messager.controller.ts | 77 ++ .../redis-configuration.repository.ts | 23 + .../commands/delete-configuration.command.ts | 9 + .../commands/set-configuration.command.ts | 9 + .../configuration/configuration.module.ts | 68 ++ .../dtos/delete-configuration.request.ts | 11 + .../domain/dtos/set-configuration.request.ts | 15 + .../domain/entities/configuration.ts | 12 + .../interfaces/configuration.repository.ts | 8 + .../usecases/delete-configuration.usecase.ts | 16 + .../usecases/get-configuration.usecase.ts | 14 + .../usecases/set-configuration.usecase.ts | 17 + .../queries/get-configuration.query.ts | 9 + .../unit/delete-configuration.usecase.spec.ts | 49 + .../unit/get-configuration.usecase.spec.ts | 43 + .../redis-configuration.repository.spec.ts | 47 + .../unit/set-configuration.usecase.spec.ts | 50 ++ src/modules/database/database.module.ts | 9 + .../secondaries/prisma-repository.abstract.ts | 202 +++++ .../adapters/secondaries/prisma-service.ts | 15 + .../database/src/domain/ad-repository.ts | 3 + .../src/exceptions/database.exception.ts | 24 + .../src/interfaces/collection.interface.ts | 4 + .../src/interfaces/repository.interface.ts | 18 + .../tests/unit/prisma-repository.spec.ts | 461 ++++++++++ .../primaries/health-server.controller.ts | 42 + .../adapters/primaries/health.controller.ts | 34 + .../health/adapters/primaries/health.proto | 21 + .../adapters/secondaries/message-broker.ts | 12 + .../health/adapters/secondaries/messager.ts | 18 + .../prisma.health-indicator.usecase.ts | 25 + src/modules/health/health.module.ts | 34 + .../health/tests/unit/messager.spec.ts | 47 + .../prisma.health-indicator.usecase.spec.ts | 58 ++ src/utils/pipes/rpc.validation-pipe.ts | 14 + .../unit/rpc-validation-pipe.usecase.spec.ts | 20 + 49 files changed, 2698 insertions(+), 29 deletions(-) create mode 100644 .env.dist create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 prisma/migrations/20230503152948_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/modules/ad/ad.module.ts create mode 100644 src/modules/ad/adapters/secondaries/ads.repository.ts create mode 100644 src/modules/ad/domain/entities/ad.ts create mode 100644 src/modules/configuration/adapters/primaries/configuration-messager.controller.ts create mode 100644 src/modules/configuration/adapters/secondaries/redis-configuration.repository.ts create mode 100644 src/modules/configuration/commands/delete-configuration.command.ts create mode 100644 src/modules/configuration/commands/set-configuration.command.ts create mode 100644 src/modules/configuration/configuration.module.ts create mode 100644 src/modules/configuration/domain/dtos/delete-configuration.request.ts create mode 100644 src/modules/configuration/domain/dtos/set-configuration.request.ts create mode 100644 src/modules/configuration/domain/entities/configuration.ts create mode 100644 src/modules/configuration/domain/interfaces/configuration.repository.ts create mode 100644 src/modules/configuration/domain/usecases/delete-configuration.usecase.ts create mode 100644 src/modules/configuration/domain/usecases/get-configuration.usecase.ts create mode 100644 src/modules/configuration/domain/usecases/set-configuration.usecase.ts create mode 100644 src/modules/configuration/queries/get-configuration.query.ts create mode 100644 src/modules/configuration/tests/unit/delete-configuration.usecase.spec.ts create mode 100644 src/modules/configuration/tests/unit/get-configuration.usecase.spec.ts create mode 100644 src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts create mode 100644 src/modules/configuration/tests/unit/set-configuration.usecase.spec.ts create mode 100644 src/modules/database/database.module.ts create mode 100644 src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts create mode 100644 src/modules/database/src/adapters/secondaries/prisma-service.ts create mode 100644 src/modules/database/src/domain/ad-repository.ts create mode 100644 src/modules/database/src/exceptions/database.exception.ts create mode 100644 src/modules/database/src/interfaces/collection.interface.ts create mode 100644 src/modules/database/src/interfaces/repository.interface.ts create mode 100644 src/modules/database/tests/unit/prisma-repository.spec.ts create mode 100644 src/modules/health/adapters/primaries/health-server.controller.ts create mode 100644 src/modules/health/adapters/primaries/health.controller.ts create mode 100644 src/modules/health/adapters/primaries/health.proto create mode 100644 src/modules/health/adapters/secondaries/message-broker.ts create mode 100644 src/modules/health/adapters/secondaries/messager.ts create mode 100644 src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts create mode 100644 src/modules/health/health.module.ts create mode 100644 src/modules/health/tests/unit/messager.spec.ts create mode 100644 src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts create mode 100644 src/utils/pipes/rpc.validation-pipe.ts create mode 100644 src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..062c6d0 --- /dev/null +++ b/.env.dist @@ -0,0 +1,17 @@ +# SERVICE +SERVICE_URL=0.0.0.0 +SERVICE_PORT=5006 +SERVICE_CONFIGURATION_DOMAIN=AD +HEALTH_SERVICE_PORT=6006 + +# PRISMA +DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=ad" + +# RABBIT MQ +RMQ_URI=amqp://v3-broker:5672 +RMQ_EXCHANGE=mobicoop + +# REDIS +REDIS_HOST=v3-redis +REDIS_PASSWORD=redis +REDIS_PORT=6379 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..59c44e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +################### +# BUILD FOR LOCAL DEVELOPMENT +################### + +FROM node:18-alpine3.16 As development + +# Create app directory +WORKDIR /usr/src/app + +# Copy application dependency manifests to the container image. +# A wildcard is used to ensure copying both package.json AND package-lock.json (when available). +# Copying this first prevents re-running npm install on every code change. +COPY --chown=node:node package*.json ./ + +# Copy prisma (needed for prisma error types) +COPY --chown=node:node ./prisma prisma + +# Install app dependencies using the `npm ci` command instead of `npm install` +RUN npm ci +RUN npx prisma generate + +# Bundle app source +COPY --chown=node:node . . + +# Use the node user from the image (instead of the root user) +USER node + +################### +# BUILD FOR PRODUCTION +################### + +FROM node:18-alpine3.16 As build + +WORKDIR /usr/src/app + +COPY --chown=node:node package*.json ./ + +# In order to run `npm run build` we need access to the Nest CLI. +# The Nest CLI is a dev dependency, +# In the previous development stage we ran `npm ci` which installed all dependencies. +# So we can copy over the node_modules directory from the development image into this build image. +COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules + +COPY --chown=node:node . . + +# Copy prisma (needed for migrations) +COPY --chown=node:node ./prisma prisma + +# Run the build command which creates the production bundle +RUN npm run build + +# Set NODE_ENV environment variable +ENV NODE_ENV production + +# Running `npm ci` removes the existing node_modules directory. +# Passing in --omit=dev ensures that only the production dependencies are installed. +# This ensures that the node_modules directory is as optimized as possible. +RUN npm ci --omit=dev && npm cache clean --force + +USER node + +################### +# PRODUCTION +################### + +FROM node:18-alpine3.16 As production + +# Copy package.json to be able to execute migration command +COPY --chown=node:node package*.json ./ + +# Copy the bundled code from the build stage to the production image +COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules +COPY --chown=node:node --from=build /usr/src/app/prisma ./prisma +COPY --chown=node:node --from=build /usr/src/app/dist ./dist + +# Start the server using the production build +CMD [ "node", "dist/main.js" ] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2c46336 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + v3-ad-api: + container_name: v3-ad-api + build: + dockerfile: Dockerfile + context: . + target: development + volumes: + - .:/usr/src/app + env_file: + - .env + command: npm run start:dev + ports: + - ${SERVICE_PORT:-5006}:${SERVICE_PORT:-5006} + - ${HEALTH_SERVICE_PORT:-6006}:${HEALTH_SERVICE_PORT:-6006} + networks: + v3-network: + aliases: + - v3-ad-api + +networks: + v3-network: + name: v3-network + external: true diff --git a/package-lock.json b/package-lock.json index 83900da..502bcca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,31 @@ { - "name": "ad", + "name": "@mobicoop/ad", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ad", + "name": "@mobicoop/ad", "version": "0.0.1", - "license": "UNLICENSED", + "license": "AGPL", "dependencies": { + "@automapper/classes": "^8.7.7", + "@automapper/core": "^8.7.7", + "@automapper/nestjs": "^8.7.7", + "@golevelup/nestjs-rabbitmq": "^3.6.0", + "@grpc/grpc-js": "^1.8.14", + "@grpc/proto-loader": "^0.7.6", + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.3.1", "@nestjs/core": "^9.0.0", + "@nestjs/cqrs": "^9.0.3", + "@nestjs/microservices": "^9.4.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/terminus": "^9.2.2", + "@prisma/client": "^4.13.0", + "class-validator": "^0.14.0", + "ioredis": "^5.3.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" }, @@ -30,6 +44,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "29.5.0", "prettier": "^2.3.2", + "prisma": "^4.13.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "29.0.5", @@ -180,6 +195,39 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@automapper/classes": { + "version": "8.7.7", + "resolved": "https://registry.npmjs.org/@automapper/classes/-/classes-8.7.7.tgz", + "integrity": "sha512-FSbvt6QE8XnhKKQZA3kpKLuLrr9x1iW+lNYTrawVLjxQ05zsCGccLxe7moMNrg1wFAVAouQKupFgCGQ7XRjmJw==", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@automapper/core": "8.7.7", + "reflect-metadata": "~0.1.13" + } + }, + "node_modules/@automapper/core": { + "version": "8.7.7", + "resolved": "https://registry.npmjs.org/@automapper/core/-/core-8.7.7.tgz", + "integrity": "sha512-YfpDJ/xqwUuC0S+BLNk81ZJfeL7CmjirUX/Gk9eQyx146DKvneBZgeZ9v5rDB51Ti14jTxVHis+5JuT7W/q0TA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@automapper/nestjs": { + "version": "8.7.7", + "resolved": "https://registry.npmjs.org/@automapper/nestjs/-/nestjs-8.7.7.tgz", + "integrity": "sha512-9/uYY2cmN7SJjr2QxnfyXsteHrn/RHD+Dg0VMBflzK/e8Bh/KWyOve7+kaFixlUoyHe44aXs2LVaCslqt8wnhQ==", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@automapper/core": "8.7.7", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", @@ -876,6 +924,111 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@golevelup/nestjs-common": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.4.tgz", + "integrity": "sha512-NTjtOhHTMuGwiR3lmBQKKaRr++mHQEsh8AxtaH+/EWOYKMK2Cv/8duaH9MQ0hI3TwnouyaA5IRxYR1ZCUyNXOQ==", + "dependencies": { + "nanoid": "^3.2.0" + } + }, + "node_modules/@golevelup/nestjs-discovery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.0.tgz", + "integrity": "sha512-ZvkXtobTKxXB1LJanP/l6Z/Fing88IMBr3uabQpU2IWjfsstjh02qYDSU2cfD6CSmNldX5ewW5Pd+SdK2lU8Sw==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/@golevelup/nestjs-modules": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.6.1.tgz", + "integrity": "sha512-E0STg8In8fhIivnGDJAA70+XLPHzK5bMTkCnif9FbZ8waTYDQ3T/QQL0h73k+CUFeznn1hmuEW14sNaE+8cd7w==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^9.x", + "rxjs": "^7.x" + } + }, + "node_modules/@golevelup/nestjs-rabbitmq": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-3.6.0.tgz", + "integrity": "sha512-gfa+27QlQdf49g9Y1JrFp/xXFjzXEtRTlVDW7KqJTUYqq03f2AKYdOPH0Iu8ScqpPPRt+qNv9COBJD44Sizerg==", + "dependencies": { + "@golevelup/nestjs-common": "^1.4.4", + "@golevelup/nestjs-discovery": "^3.0.0", + "@golevelup/nestjs-modules": "^0.6.1", + "amqp-connection-manager": "^3.0.0", + "amqplib": "^0.8.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.14.tgz", + "integrity": "sha512-w84maJ6CKl5aApCMzFll0hxtFNT6or9WwMslobKaqWUEf1K+zhlL43bSQhFreyYWIWR+Z0xnVFC1KtLm4ZpM/A==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.6.tgz", + "integrity": "sha512-QyAXR8Hyh7uMDmveWxDSUcJr9NAWaZ2I6IXgAYvQmfflwouTM+rArE2eEaCtLlRqO81j7pRLCt81IefUei6Zbw==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -909,6 +1062,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/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1359,6 +1517,27 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "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/@liaoliaots/nestjs-redis/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -1482,6 +1661,22 @@ } } }, + "node_modules/@nestjs/config": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-2.3.1.tgz", + "integrity": "sha512-Ckzel0NZ9CWhNsLfE1hxfDuxJuEbhQvGxSlmZ1/X8awjRmAA/g3kT6M1+MO1SHj1wMtPyUfd9WpwkiqFbiwQgA==", + "dependencies": { + "dotenv": "16.0.3", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^6.0.0 || ^7.2.0" + } + }, "node_modules/@nestjs/core": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.4.0.tgz", @@ -1519,6 +1714,77 @@ } } }, + "node_modules/@nestjs/cqrs": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/cqrs/-/cqrs-9.0.3.tgz", + "integrity": "sha512-hmbrqf51BVdgmnnxErnLVXfPNTEqr4Hz8DyLa9dKLIW3BuOyI5RDwJ/9sKbJ47UDBhumC5nQlNK9qk27mhqHfw==", + "dependencies": { + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "reflect-metadata": "0.1.13", + "rxjs": "^7.2.0" + } + }, + "node_modules/@nestjs/microservices": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.0.tgz", + "integrity": "sha512-3IlURTijN2whedrfnLbJ3QQ4giDU1SxXcepXxtUL1MMkZAJgw2gN7sTquOXVgy/Ci5OMPO+vOjVyadjFejrgKA==", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "@nestjs/websockets": "^9.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.0.tgz", @@ -1616,6 +1882,71 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@nestjs/terminus": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-9.2.2.tgz", + "integrity": "sha512-AWUA8XLcgxWUjUFYHDqi42M7CZn2e+DEWxP+MqNAbMzz4ybB5jGcFK5Fy8qwaNBoWg6KMF1JiXOOygGXgk9ydg==", + "dependencies": { + "boxen": "5.1.2", + "check-disk-space": "3.3.1" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@grpc/proto-loader": "*", + "@mikro-orm/core": "*", + "@mikro-orm/nestjs": "*", + "@nestjs/axios": "*", + "@nestjs/common": "9.x", + "@nestjs/core": "9.x", + "@nestjs/microservices": "*", + "@nestjs/mongoose": "*", + "@nestjs/sequelize": "*", + "@nestjs/typeorm": "*", + "mongoose": "*", + "reflect-metadata": "0.1.x", + "rxjs": "7.x", + "sequelize": "*", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@grpc/proto-loader": { + "optional": true + }, + "@mikro-orm/core": { + "optional": true + }, + "@mikro-orm/nestjs": { + "optional": true + }, + "@nestjs/axios": { + "optional": true + }, + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/mongoose": { + "optional": true + }, + "@nestjs/sequelize": { + "optional": true + }, + "@nestjs/typeorm": { + "optional": true + }, + "mongoose": { + "optional": true + }, + "sequelize": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.4.0.tgz", @@ -1695,6 +2026,92 @@ "npm": ">=5.0.0" } }, + "node_modules/@prisma/client": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1908,6 +2325,11 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -1917,8 +2339,7 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -1995,6 +2416,11 @@ "@types/superagent": "*" } }, + "node_modules/@types/validator": { + "version": "13.7.15", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz", + "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -2440,6 +2866,66 @@ } } }, + "node_modules/amqp-connection-manager": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-3.9.0.tgz", + "integrity": "sha512-ZKw9ckJKz40Lc2pC7DY0NVocpzPalMaCgv0sBn+N4er2QFAJul9pIiMOm/FsPHeCzB+FulV7PckOpmZvWvewGQ==", + "dependencies": { + "promise-breaker": "^5.0.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">5.0.0" + }, + "peerDependencies": { + "amqplib": "*" + } + }, + "node_modules/amqplib": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.8.0.tgz", + "integrity": "sha512-icU+a4kkq4Y1PS4NNi+YPDMwdlbFcZ1EZTQT2nigW3fvOb6AOgUQ9+Mk4ue0Zu5cBg/XpDzB40oH10ysrk2dmA==", + "dependencies": { + "bitsyntax": "~0.1.0", + "bluebird": "^3.7.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "safe-buffer": "~5.2.1", + "url-parse": "~1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/amqplib/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/amqplib/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/amqplib/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -2480,7 +2966,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2681,6 +3166,37 @@ "node": ">=8" } }, + "node_modules/bitsyntax": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.1.0.tgz", + "integrity": "sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q==", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "~2.6.9", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/bitsyntax/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/bitsyntax/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2706,6 +3222,11 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -2742,6 +3263,38 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2842,6 +3395,11 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2941,6 +3499,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-disk-space": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.3.1.tgz", + "integrity": "sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2998,6 +3564,27 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -3069,6 +3656,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", @@ -3254,7 +3849,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3309,6 +3903,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", @@ -3387,6 +3989,22 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3413,8 +4031,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -3465,7 +4082,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -4248,7 +4864,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -4575,6 +5190,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", @@ -4626,7 +5264,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -5502,6 +6139,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.28", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.28.tgz", + "integrity": "sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5535,8 +6177,22 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "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", @@ -5566,6 +6222,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5781,8 +6442,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -5807,6 +6467,23 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6336,11 +7013,33 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.13.0" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6354,6 +7053,34 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", + "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6415,6 +7142,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6515,6 +7247,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", @@ -6524,7 +7275,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6538,6 +7288,11 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -7019,6 +7774,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", @@ -7065,7 +7825,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7079,7 +7838,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7571,7 +8329,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -7676,6 +8433,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7689,6 +8455,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -7715,6 +8489,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7849,6 +8631,17 @@ "node": ">= 8" } }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/windows-release": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", @@ -7924,7 +8717,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7968,7 +8760,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } diff --git a/package.json b/package.json index 0f8e819..c3ce7bd 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "license": "AGPL", "scripts": { + "prebuild": "rimraf dist", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", @@ -13,16 +14,40 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore", + "pretty:check": "./node_modules/.bin/prettier --check .", + "pretty": "./node_modules/.bin/prettier --write .", + "test": "npm run migrate:test && dotenv -e .env.test jest", + "test:unit": "jest --testPathPattern 'tests/unit/' --verbose", + "test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage", + "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", + "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-ad-api sh -c 'npx prisma generate'", + "migrate": "docker exec v3-ad-api 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", + "migrate:deploy": "npx prisma migrate deploy" }, "dependencies": { + "@automapper/classes": "^8.7.7", + "@automapper/core": "^8.7.7", + "@automapper/nestjs": "^8.7.7", + "@golevelup/nestjs-rabbitmq": "^3.6.0", + "@grpc/grpc-js": "^1.8.14", + "@grpc/proto-loader": "^0.7.6", + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.3.1", "@nestjs/core": "^9.0.0", + "@nestjs/cqrs": "^9.0.3", + "@nestjs/microservices": "^9.4.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/terminus": "^9.2.2", + "@prisma/client": "^4.13.0", + "class-validator": "^0.14.0", + "ioredis": "^5.3.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" }, @@ -41,6 +66,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "29.5.0", "prettier": "^2.3.2", + "prisma": "^4.13.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "29.0.5", @@ -55,6 +81,15 @@ "json", "ts" ], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + ".presenter.ts", + ".profile.ts", + ".exception.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { @@ -63,6 +98,15 @@ "collectCoverageFrom": [ "**/*.(t|j)s" ], + "coveragePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + ".presenter.ts", + ".profile.ts", + ".exception.ts", + "main.ts" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/prisma/migrations/20230503152948_init/migration.sql b/prisma/migrations/20230503152948_init/migration.sql new file mode 100644 index 0000000..d6acd25 --- /dev/null +++ b/prisma/migrations/20230503152948_init/migration.sql @@ -0,0 +1,60 @@ +-- CreateTable +CREATE TABLE "ad" ( + "uuid" UUID NOT NULL, + "userUuid" UUID NOT NULL, + "driver" BOOLEAN NOT NULL, + "passenger" BOOLEAN NOT NULL, + "frequency" INTEGER NOT NULL, + "fromDate" DATE NOT NULL, + "toDate" DATE NOT NULL, + "monTime" TIMESTAMPTZ NOT NULL, + "tueTime" TIMESTAMPTZ NOT NULL, + "wedTime" TIMESTAMPTZ NOT NULL, + "thuTime" TIMESTAMPTZ NOT NULL, + "friTime" TIMESTAMPTZ NOT NULL, + "satTime" TIMESTAMPTZ NOT NULL, + "sunTime" TIMESTAMPTZ NOT NULL, + "monMargin" INTEGER NOT NULL, + "tueMargin" INTEGER NOT NULL, + "wedMargin" INTEGER NOT NULL, + "thuMargin" INTEGER NOT NULL, + "friMargin" INTEGER NOT NULL, + "satMargin" INTEGER NOT NULL, + "sunMargin" INTEGER NOT NULL, + "seatsDriver" SMALLINT NOT NULL, + "seatsPassenger" SMALLINT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid") +); + +-- CreateTable +CREATE TABLE "address" ( + "uuid" UUID NOT NULL, + "adUuid" UUID NOT NULL, + "lon" DOUBLE PRECISION NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "houseNumber" TEXT NOT NULL, + "street" TEXT NOT NULL, + "locality" TEXT NOT NULL, + "postalCode" TEXT NOT NULL, + "country" TEXT NOT NULL, + "type" SMALLINT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "address_pkey" PRIMARY KEY ("uuid") +); + +-- CreateIndex +CREATE INDEX "ad_driver_idx" ON "ad"("driver"); + +-- CreateIndex +CREATE INDEX "ad_passenger_idx" ON "ad"("passenger"); + +-- CreateIndex +CREATE INDEX "ad_fromDate_idx" ON "ad"("fromDate"); + +-- CreateIndex +CREATE INDEX "ad_toDate_idx" ON "ad"("toDate"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# 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 new file mode 100644 index 0000000..f5ace57 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,62 @@ +// 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" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Ad { + uuid String @id @default(uuid()) @db.Uuid + userUuid String @db.Uuid + driver Boolean + passenger Boolean + frequency Int + fromDate DateTime @db.Date + toDate DateTime @db.Date + monTime DateTime @db.Timestamptz() + tueTime DateTime @db.Timestamptz() + wedTime DateTime @db.Timestamptz() + thuTime DateTime @db.Timestamptz() + friTime DateTime @db.Timestamptz() + satTime DateTime @db.Timestamptz() + sunTime DateTime @db.Timestamptz() + monMargin Int + tueMargin Int + wedMargin Int + thuMargin Int + friMargin Int + satMargin Int + sunMargin Int + seatsDriver Int @db.SmallInt + seatsPassenger Int @db.SmallInt + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@index([driver]) + @@index([passenger]) + @@index([fromDate]) + @@index([toDate]) + @@map("ad") +} + +model Address { + uuid String @id @default(uuid()) @db.Uuid + adUuid String @db.Uuid + lon Float + lat Float + houseNumber String + street String + locality String + postalCode String + country String + type Int @db.SmallInt + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@map("address") +} diff --git a/src/app.module.ts b/src/app.module.ts index ee5f2c9..bfca0d7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,16 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ConfigurationModule } from './modules/configuration/configuration.module'; +// import { HealthModule } from './modules/health/health.module'; +import { AdModule } from './modules/ad/ad.module'; @Module({ - imports: [], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + ConfigurationModule, + // HealthModule, + AdModule, + ], controllers: [], providers: [], }) diff --git a/src/main.ts b/src/main.ts index 13cad38..3b27143 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,28 @@ import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { join } from 'path'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3000); + app.connectMicroservice({ + transport: Transport.TCP, + }); + app.connectMicroservice({ + transport: Transport.GRPC, + options: { + // package: ['ad', 'health'], + package: ['health'], + protoPath: [ + // join(__dirname, 'modules/ad/adapters/primaries/ad.proto'), + join(__dirname, 'modules/health/adapters/primaries/health.proto'), + ], + url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, + loader: { keepCase: true }, + }, + }); + + await app.startAllMicroservices(); + await app.listen(process.env.HEALTH_SERVICE_PORT); } bootstrap(); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts new file mode 100644 index 0000000..8946839 --- /dev/null +++ b/src/modules/ad/ad.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +@Module({ + imports: [], + controllers: [], + providers: [], +}) +export class AdModule {} diff --git a/src/modules/ad/adapters/secondaries/ads.repository.ts b/src/modules/ad/adapters/secondaries/ads.repository.ts new file mode 100644 index 0000000..3b6e9a5 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/ads.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { AdRepository } from '../../../database/src/domain/ad-repository'; +import { Ad } from '../../domain/entities/ad'; + +@Injectable() +export class AdsRepository extends AdRepository { + protected _model = 'ad'; +} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts new file mode 100644 index 0000000..0350f1a --- /dev/null +++ b/src/modules/ad/domain/entities/ad.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class Ad { + @AutoMap() + uuid: string; +} 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..c9408ca --- /dev/null +++ b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts @@ -0,0 +1,77 @@ +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'; +import { Configuration } from '../../domain/entities/configuration'; + +@Controller() +export class ConfigurationMessagerController { + constructor( + private readonly _commandBus: CommandBus, + private readonly _configService: ConfigService, + ) {} + + @RabbitSubscribe({ + name: 'setConfiguration', + }) + public async setConfigurationHandler(message: string) { + const configuration: 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({ + name: 'deleteConfiguration', + }) + public async configurationDeletedHandler(message: string) { + const deletedConfiguration: Configuration = 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), + ); + } + } + + @RabbitSubscribe({ + name: 'propagateConfiguration', + }) + public async propagateConfigurationsHandler(message: string) { + const configurations: Array = JSON.parse(message); + configurations.forEach(async (configuration) => { + 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), + ); + } + }); + } +} 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..fab6fe0 --- /dev/null +++ b/src/modules/configuration/configuration.module.ts @@ -0,0 +1,68 @@ +import { RabbitMQConfig, 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 => ({ + config: { + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + password: configService.get('REDIS_PASSWORD'), + }, + }), + }), + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async ( + configService: ConfigService, + ): Promise => ({ + exchanges: [ + { + name: configService.get('RMQ_EXCHANGE'), + type: 'topic', + }, + ], + handlers: { + setConfiguration: { + exchange: configService.get('RMQ_EXCHANGE'), + routingKey: ['configuration.create', 'configuration.update'], + }, + deleteConfiguration: { + exchange: configService.get('RMQ_EXCHANGE'), + routingKey: 'configuration.delete', + }, + propagateConfiguration: { + exchange: configService.get('RMQ_EXCHANGE'), + routingKey: 'configuration.propagate', + }, + }, + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + enableControllerDiscovery: true, + }), + }), + ], + 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..2008403 --- /dev/null +++ b/src/modules/configuration/domain/entities/configuration.ts @@ -0,0 +1,12 @@ +import { AutoMap } from '@automapper/classes'; + +export class Configuration { + @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/redis-configuration.repository.spec.ts b/src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts new file mode 100644 index 0000000..8efa436 --- /dev/null +++ b/src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { getRedisToken } from '@liaoliaots/nestjs-redis'; + +const mockRedis = { + get: jest.fn().mockResolvedValue('myValue'), + set: jest.fn().mockImplementation(), + del: jest.fn().mockImplementation(), +}; + +describe('RedisConfigurationRepository', () => { + let redisConfigurationRepository: RedisConfigurationRepository; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRedisToken('default'), + useValue: mockRedis, + }, + RedisConfigurationRepository, + ], + }).compile(); + + redisConfigurationRepository = module.get( + RedisConfigurationRepository, + ); + }); + + it('should be defined', () => { + expect(redisConfigurationRepository).toBeDefined(); + }); + + describe('interact', () => { + it('should get a value', async () => { + expect(await redisConfigurationRepository.get('myKey')).toBe('myValue'); + }); + it('should set a value', async () => { + expect( + await redisConfigurationRepository.set('myKey', 'myValue'), + ).toBeUndefined(); + }); + it('should delete a value', async () => { + expect(await redisConfigurationRepository.del('myKey')).toBeUndefined(); + }); + }); +}); 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); + }); + }); +}); diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts new file mode 100644 index 0000000..90b954b --- /dev/null +++ b/src/modules/database/database.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from './src/adapters/secondaries/prisma-service'; +import { AdRepository } from './src/domain/ad-repository'; + +@Module({ + providers: [PrismaService, AdRepository], + exports: [PrismaService, AdRepository], +}) +export class DatabaseModule {} diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts new file mode 100644 index 0000000..c14086d --- /dev/null +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { DatabaseException } from '../../exceptions/database.exception'; +import { ICollection } from '../../interfaces/collection.interface'; +import { IRepository } from '../../interfaces/repository.interface'; +import { PrismaService } from './prisma-service'; + +/** + * Child classes MUST redefined _model property with appropriate model name + */ +@Injectable() +export abstract class PrismaRepository implements IRepository { + protected _model: string; + + constructor(protected readonly _prisma: PrismaService) {} + + async findAll( + page = 1, + perPage = 10, + where?: any, + include?: any, + ): Promise> { + const [data, total] = await this._prisma.$transaction([ + this._prisma[this._model].findMany({ + where, + include, + skip: (page - 1) * perPage, + take: perPage, + }), + this._prisma[this._model].count({ + where, + }), + ]); + return Promise.resolve({ + data, + total, + }); + } + + async findOneByUuid(uuid: string): Promise { + try { + const entity = await this._prisma[this._model].findUnique({ + where: { uuid }, + }); + + return entity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async findOne(where: any, include?: any): Promise { + try { + const entity = await this._prisma[this._model].findFirst({ + where: where, + include: include, + }); + + return entity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException(PrismaClientKnownRequestError.name, e.code); + } else { + throw new DatabaseException(); + } + } + } + + // TODO : using any is not good, but needed for nested entities + // TODO : Refactor for good clean architecture ? + async create(entity: Partial | any, include?: any): Promise { + try { + const res = await this._prisma[this._model].create({ + data: entity, + include: include, + }); + + return res; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async update(uuid: string, entity: Partial): Promise { + try { + const updatedEntity = await this._prisma[this._model].update({ + where: { uuid }, + data: entity, + }); + return updatedEntity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async updateWhere( + where: any, + entity: Partial | any, + include?: any, + ): Promise { + try { + const updatedEntity = await this._prisma[this._model].update({ + where: where, + data: entity, + include: include, + }); + + return updatedEntity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async delete(uuid: string): Promise { + try { + const entity = await this._prisma[this._model].delete({ + where: { uuid }, + }); + + return entity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async deleteMany(where: any): Promise { + try { + const entity = await this._prisma[this._model].deleteMany({ + where: where, + }); + + return entity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async healthCheck(): Promise { + try { + await this._prisma.$queryRaw`SELECT 1`; + return true; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } +} diff --git a/src/modules/database/src/adapters/secondaries/prisma-service.ts b/src/modules/database/src/adapters/secondaries/prisma-service.ts new file mode 100644 index 0000000..edf6532 --- /dev/null +++ b/src/modules/database/src/adapters/secondaries/prisma-service.ts @@ -0,0 +1,15 @@ +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => { + await app.close(); + }); + } +} diff --git a/src/modules/database/src/domain/ad-repository.ts b/src/modules/database/src/domain/ad-repository.ts new file mode 100644 index 0000000..edbaf5f --- /dev/null +++ b/src/modules/database/src/domain/ad-repository.ts @@ -0,0 +1,3 @@ +import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; + +export class AdRepository extends PrismaRepository {} diff --git a/src/modules/database/src/exceptions/database.exception.ts b/src/modules/database/src/exceptions/database.exception.ts new file mode 100644 index 0000000..b0782a6 --- /dev/null +++ b/src/modules/database/src/exceptions/database.exception.ts @@ -0,0 +1,24 @@ +export class DatabaseException implements Error { + name: string; + message: string; + + constructor( + private _type: string = 'unknown', + private _code: string = '', + message?: string, + ) { + this.name = 'DatabaseException'; + this.message = message ?? 'An error occured with the database.'; + if (this.message.includes('Unique constraint failed')) { + this.message = 'Already exists.'; + } + } + + get type(): string { + return this._type; + } + + get code(): string { + return this._code; + } +} diff --git a/src/modules/database/src/interfaces/collection.interface.ts b/src/modules/database/src/interfaces/collection.interface.ts new file mode 100644 index 0000000..6e9a96d --- /dev/null +++ b/src/modules/database/src/interfaces/collection.interface.ts @@ -0,0 +1,4 @@ +export interface ICollection { + data: T[]; + total: number; +} diff --git a/src/modules/database/src/interfaces/repository.interface.ts b/src/modules/database/src/interfaces/repository.interface.ts new file mode 100644 index 0000000..1e23984 --- /dev/null +++ b/src/modules/database/src/interfaces/repository.interface.ts @@ -0,0 +1,18 @@ +import { ICollection } from './collection.interface'; + +export interface IRepository { + findAll( + page: number, + perPage: number, + params?: any, + include?: any, + ): Promise>; + findOne(where: any, include?: any): Promise; + findOneByUuid(uuid: string, include?: any): Promise; + create(entity: Partial | any, include?: any): Promise; + update(uuid: string, entity: Partial, include?: any): Promise; + updateWhere(where: any, entity: Partial | any, include?: any): Promise; + delete(uuid: string): Promise; + deleteMany(where: any): Promise; + healthCheck(): Promise; +} diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts new file mode 100644 index 0000000..3e2be4a --- /dev/null +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -0,0 +1,461 @@ +import { Injectable } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; +import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; +import { DatabaseException } from '../../src/exceptions/database.exception'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; + +class FakeEntity { + uuid?: string; + name: string; +} + +let entityId = 2; +const entityUuid = 'uuid-'; +const entityName = 'name-'; + +const createRandomEntity = (): FakeEntity => { + const entity: FakeEntity = { + uuid: `${entityUuid}${entityId}`, + name: `${entityName}${entityId}`, + }; + + entityId++; + + return entity; +}; + +const fakeEntityToCreate: FakeEntity = { + name: 'test', +}; + +const fakeEntityCreated: FakeEntity = { + ...fakeEntityToCreate, + uuid: 'some-uuid', +}; + +const fakeEntities: FakeEntity[] = []; +Array.from({ length: 10 }).forEach(() => { + fakeEntities.push(createRandomEntity()); +}); + +@Injectable() +class FakePrismaRepository extends PrismaRepository { + protected _model = 'fake'; +} + +class FakePrismaService extends PrismaService { + fake: any; +} + +const mockPrismaService = { + $transaction: jest.fn().mockImplementation(async (data: any) => { + const entities = await data[0]; + if (entities.length == 1) { + return Promise.resolve([[fakeEntityCreated], 1]); + } + + return Promise.resolve([fakeEntities, fakeEntities.length]); + }), + $queryRaw: jest + .fn() + .mockImplementationOnce(() => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementationOnce(() => { + return true; + }) + .mockImplementation(() => { + throw new PrismaClientKnownRequestError('Database unavailable', { + code: 'code', + clientVersion: 'version', + }); + }), + fake: { + create: jest + .fn() + .mockResolvedValueOnce(fakeEntityCreated) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new Error('an unknown error'); + }), + + findMany: jest.fn().mockImplementation((params?: any) => { + if (params?.where?.limit == 1) { + return Promise.resolve([fakeEntityCreated]); + } + + return Promise.resolve(fakeEntities); + }), + count: jest.fn().mockResolvedValue(fakeEntities.length), + + findUnique: jest.fn().mockImplementation(async (params?: any) => { + let entity; + + if (params?.where?.uuid) { + entity = fakeEntities.find( + (entity) => entity.uuid === params?.where?.uuid, + ); + } + + if (!entity && params?.where?.uuid == 'unknown') { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + } else if (!entity) { + throw new Error('no entity'); + } + + return entity; + }), + + findFirst: jest + .fn() + .mockImplementationOnce((params?: any) => { + if (params?.where?.name) { + return Promise.resolve( + fakeEntities.find((entity) => entity.name === params?.where?.name), + ); + } + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new Error('an unknown error'); + }), + + update: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementationOnce((params: any) => { + const entity = fakeEntities.find( + (entity) => entity.name === params.where.name, + ); + Object.entries(params.data).map(([key, value]) => { + entity[key] = value; + }); + + return Promise.resolve(entity); + }) + .mockImplementation((params: any) => { + const entity = fakeEntities.find( + (entity) => entity.uuid === params.where.uuid, + ); + Object.entries(params.data).map(([key, value]) => { + entity[key] = value; + }); + + return Promise.resolve(entity); + }), + + delete: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementation((params: any) => { + let found = false; + + fakeEntities.forEach((entity, index) => { + if (entity.uuid === params?.where?.uuid) { + found = true; + fakeEntities.splice(index, 1); + } + }); + + if (!found) { + throw new Error(); + } + }), + + deleteMany: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementation((params: any) => { + let found = false; + + fakeEntities.forEach((entity, index) => { + if (entity.uuid === params?.where?.uuid) { + found = true; + fakeEntities.splice(index, 1); + } + }); + + if (!found) { + throw new Error(); + } + }), + }, +}; + +describe('PrismaRepository', () => { + let fakeRepository: FakePrismaRepository; + let prisma: FakePrismaService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FakePrismaRepository, + { + provide: PrismaService, + useValue: mockPrismaService, + }, + ], + }).compile(); + + fakeRepository = module.get(FakePrismaRepository); + prisma = module.get(PrismaService) as FakePrismaService; + }); + + it('should be defined', () => { + expect(fakeRepository).toBeDefined(); + expect(prisma).toBeDefined(); + }); + + describe('findAll', () => { + it('should return an array of entities', async () => { + jest.spyOn(prisma.fake, 'findMany'); + jest.spyOn(prisma.fake, 'count'); + jest.spyOn(prisma, '$transaction'); + + const entities = await fakeRepository.findAll(); + expect(entities).toStrictEqual({ + data: fakeEntities, + total: fakeEntities.length, + }); + }); + + it('should return an array containing only one entity', async () => { + const entities = await fakeRepository.findAll(1, 10, { limit: 1 }); + + expect(prisma.fake.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 10, + where: { limit: 1 }, + }); + expect(entities).toEqual({ + data: [fakeEntityCreated], + total: 1, + }); + }); + }); + + describe('create', () => { + it('should create an entity', async () => { + jest.spyOn(prisma.fake, 'create'); + + const newEntity = await fakeRepository.create(fakeEntityToCreate); + expect(newEntity).toBe(fakeEntityCreated); + expect(prisma.fake.create).toHaveBeenCalledTimes(1); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.create(fakeEntityToCreate), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.create(fakeEntityToCreate), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('findOneByUuid', () => { + it('should find an entity by uuid', async () => { + const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid); + expect(entity).toBe(fakeEntities[0]); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.findOneByUuid('unknown'), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.findOneByUuid('wrong-uuid'), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('findOne', () => { + it('should find one entity', async () => { + const entity = await fakeRepository.findOne({ + name: fakeEntities[0].name, + }); + + expect(entity.name).toBe(fakeEntities[0].name); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.findOne({ + name: fakeEntities[0].name, + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException for unknown error', async () => { + await expect( + fakeRepository.findOne({ + name: fakeEntities[0].name, + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('update', () => { + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.update('fake-uuid', { name: 'error' }), + ).rejects.toBeInstanceOf(DatabaseException); + await expect( + fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should update an entity with name', async () => { + const newName = 'new-random-name'; + + await fakeRepository.updateWhere( + { name: fakeEntities[0].name }, + { + name: newName, + }, + ); + expect(fakeEntities[0].name).toBe(newName); + }); + + it('should update an entity with uuid', async () => { + const newName = 'random-name'; + + await fakeRepository.update(fakeEntities[0].uuid, { + name: newName, + }); + expect(fakeEntities[0].name).toBe(newName); + }); + + it("should throw an exception if an entity doesn't exist", async () => { + await expect( + fakeRepository.update('fake-uuid', { name: 'error' }), + ).rejects.toBeInstanceOf(DatabaseException); + await expect( + fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('delete', () => { + it('should throw a DatabaseException for client error', async () => { + await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf( + DatabaseException, + ); + }); + + it('should delete an entity', async () => { + const savedUuid = fakeEntities[0].uuid; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const res = await fakeRepository.delete(savedUuid); + + const deletedEntity = fakeEntities.find( + (entity) => entity.uuid === savedUuid, + ); + expect(deletedEntity).toBeUndefined(); + }); + + it("should throw an exception if an entity doesn't exist", async () => { + await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf( + DatabaseException, + ); + }); + }); + + describe('deleteMany', () => { + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should delete entities based on their uuid', async () => { + const savedUuid = fakeEntities[0].uuid; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const res = await fakeRepository.deleteMany({ uuid: savedUuid }); + + const deletedEntity = fakeEntities.find( + (entity) => entity.uuid === savedUuid, + ); + expect(deletedEntity).toBeUndefined(); + }); + + it("should throw an exception if an entity doesn't exist", async () => { + await expect( + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('healthCheck', () => { + it('should throw a DatabaseException for client error', async () => { + await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( + DatabaseException, + ); + }); + + it('should return a healthy result', async () => { + const res = await fakeRepository.healthCheck(); + expect(res).toBeTruthy(); + }); + + it('should throw an exception if database is not available', async () => { + await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( + DatabaseException, + ); + }); + }); +}); diff --git a/src/modules/health/adapters/primaries/health-server.controller.ts b/src/modules/health/adapters/primaries/health-server.controller.ts new file mode 100644 index 0000000..b58c761 --- /dev/null +++ b/src/modules/health/adapters/primaries/health-server.controller.ts @@ -0,0 +1,42 @@ +import { Controller } from '@nestjs/common'; +import { GrpcMethod } from '@nestjs/microservices'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; + +enum ServingStatus { + UNKNOWN = 0, + SERVING = 1, + NOT_SERVING = 2, +} + +interface HealthCheckRequest { + service: string; +} + +interface HealthCheckResponse { + status: ServingStatus; +} + +@Controller() +export class HealthServerController { + constructor( + private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + ) {} + + @GrpcMethod('Health', 'Check') + async check( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: HealthCheckRequest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + metadata: any, + ): Promise { + const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy( + 'prisma', + ); + return { + status: + healthCheck['prisma'].status == 'up' + ? ServingStatus.SERVING + : ServingStatus.NOT_SERVING, + }; + } +} diff --git a/src/modules/health/adapters/primaries/health.controller.ts b/src/modules/health/adapters/primaries/health.controller.ts new file mode 100644 index 0000000..ee45f63 --- /dev/null +++ b/src/modules/health/adapters/primaries/health.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheckService, + HealthCheck, + HealthCheckResult, +} from '@nestjs/terminus'; +import { Messager } from '../secondaries/messager'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; + +@Controller('health') +export class HealthController { + constructor( + private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + private _healthCheckService: HealthCheckService, + private _messager: Messager, + ) {} + + @Get() + @HealthCheck() + async check() { + try { + return await this._healthCheckService.check([ + async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'), + ]); + } catch (error) { + const healthCheckResult: HealthCheckResult = error.response; + this._messager.publish( + 'logging.user.health.crit', + JSON.stringify(healthCheckResult.error), + ); + throw error; + } + } +} diff --git a/src/modules/health/adapters/primaries/health.proto b/src/modules/health/adapters/primaries/health.proto new file mode 100644 index 0000000..74e1a4c --- /dev/null +++ b/src/modules/health/adapters/primaries/health.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package health; + + +service Health { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + } + ServingStatus status = 1; +} diff --git a/src/modules/health/adapters/secondaries/message-broker.ts b/src/modules/health/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..594aa43 --- /dev/null +++ b/src/modules/health/adapters/secondaries/message-broker.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export abstract class IMessageBroker { + exchange: string; + + constructor(exchange: string) { + this.exchange = exchange; + } + + abstract publish(routingKey: string, message: string): void; +} diff --git a/src/modules/health/adapters/secondaries/messager.ts b/src/modules/health/adapters/secondaries/messager.ts new file mode 100644 index 0000000..0725261 --- /dev/null +++ b/src/modules/health/adapters/secondaries/messager.ts @@ -0,0 +1,18 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IMessageBroker } from './message-broker'; + +@Injectable() +export class Messager extends IMessageBroker { + constructor( + private readonly _amqpConnection: AmqpConnection, + configService: ConfigService, + ) { + super(configService.get('RMQ_EXCHANGE')); + } + + publish(routingKey: string, message: string): void { + this._amqpConnection.publish(this.exchange, routingKey, message); + } +} diff --git a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts new file mode 100644 index 0000000..85a1868 --- /dev/null +++ b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { AdsRepository } from '../../../ad/adapters/secondaries/ads.repository'; + +@Injectable() +export class PrismaHealthIndicatorUseCase extends HealthIndicator { + constructor(private readonly _repository: AdsRepository) { + super(); + } + + async isHealthy(key: string): Promise { + try { + await this._repository.healthCheck(); + return this.getStatus(key, true); + } catch (e) { + throw new HealthCheckError('Prisma', { + prisma: e.message, + }); + } + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..5b4ef40 --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { HealthServerController } from './adapters/primaries/health-server.controller'; +import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase'; +import { AdsRepository } from '../ad/adapters/secondaries/ads.repository'; +import { DatabaseModule } from '../database/database.module'; +import { HealthController } from './adapters/primaries/health.controller'; +import { TerminusModule } from '@nestjs/terminus'; +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Messager } from './adapters/secondaries/messager'; + +@Module({ + imports: [ + TerminusModule, + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + exchanges: [ + { + name: configService.get('RMQ_EXCHANGE'), + type: 'topic', + }, + ], + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + }), + inject: [ConfigService], + }), + DatabaseModule, + ], + controllers: [HealthServerController, HealthController], + providers: [PrismaHealthIndicatorUseCase, AdsRepository, Messager], +}) +export class HealthModule {} diff --git a/src/modules/health/tests/unit/messager.spec.ts b/src/modules/health/tests/unit/messager.spec.ts new file mode 100644 index 0000000..0331332 --- /dev/null +++ b/src/modules/health/tests/unit/messager.spec.ts @@ -0,0 +1,47 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Messager } from '../../adapters/secondaries/messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +const mockConfigService = { + get: jest.fn().mockResolvedValue({ + RMQ_EXCHANGE: 'mobicoop', + }), +}; + +describe('Messager', () => { + let messager: Messager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + Messager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + messager = module.get(Messager); + }); + + it('should be defined', () => { + expect(messager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + messager.publish('test.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts new file mode 100644 index 0000000..e62feb3 --- /dev/null +++ b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; +import { UsersRepository } from '../../../user/adapters/secondaries/users.repository'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; + +const mockUsersRepository = { + healthCheck: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve(true); + }) + .mockImplementation(() => { + throw new PrismaClientKnownRequestError('Service unavailable', { + code: 'code', + clientVersion: 'version', + }); + }), +}; + +describe('PrismaHealthIndicatorUseCase', () => { + let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: UsersRepository, + useValue: mockUsersRepository, + }, + PrismaHealthIndicatorUseCase, + ], + }).compile(); + + prismaHealthIndicatorUseCase = module.get( + PrismaHealthIndicatorUseCase, + ); + }); + + it('should be defined', () => { + expect(prismaHealthIndicatorUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should check health successfully', async () => { + const healthIndicatorResult: HealthIndicatorResult = + await prismaHealthIndicatorUseCase.isHealthy('prisma'); + + expect(healthIndicatorResult['prisma'].status).toBe('up'); + }); + + it('should throw an error if database is unavailable', async () => { + await expect( + prismaHealthIndicatorUseCase.isHealthy('prisma'), + ).rejects.toBeInstanceOf(HealthCheckError); + }); + }); +}); diff --git a/src/utils/pipes/rpc.validation-pipe.ts b/src/utils/pipes/rpc.validation-pipe.ts new file mode 100644 index 0000000..f2b8c19 --- /dev/null +++ b/src/utils/pipes/rpc.validation-pipe.ts @@ -0,0 +1,14 @@ +import { Injectable, ValidationPipe } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; + +@Injectable() +export class RpcValidationPipe extends ValidationPipe { + createExceptionFactory() { + return (validationErrors = []) => { + return new RpcException({ + code: 3, + message: this.flattenValidationErrors(validationErrors), + }); + }; + } +} diff --git a/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts new file mode 100644 index 0000000..b00bf86 --- /dev/null +++ b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,20 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { UpdateUserRequest } from '../../../modules/user/domain/dtos/update-user.request'; +import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; + +describe('RpcValidationPipe', () => { + it('should not validate request', async () => { + const target: RpcValidationPipe = new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }); + const metadata: ArgumentMetadata = { + type: 'body', + metatype: UpdateUserRequest, + data: '', + }; + await target.transform({}, metadata).catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +});