Merge branch 'install' into 'main'
Install See merge request v3/service/ad!1
This commit is contained in:
		
						commit
						0f362df74f
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
image: docker:20.10.22
 | 
			
		||||
 | 
			
		||||
stages:
 | 
			
		||||
  - test
 | 
			
		||||
  - build
 | 
			
		||||
 | 
			
		||||
##############
 | 
			
		||||
# TEST STAGE #
 | 
			
		||||
##############
 | 
			
		||||
 | 
			
		||||
test:
 | 
			
		||||
  stage: test
 | 
			
		||||
  image: docker/compose:latest
 | 
			
		||||
  variables:
 | 
			
		||||
    DOCKER_TLS_CERTDIR: ''
 | 
			
		||||
  services:
 | 
			
		||||
    - docker:dind
 | 
			
		||||
  script:
 | 
			
		||||
    - docker-compose -f docker-compose.ci.tools.yml -p ad-tools --env-file ci/.env.ci up -d
 | 
			
		||||
    - sh ci/wait-up.sh
 | 
			
		||||
    - docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d
 | 
			
		||||
    # - docker exec -t v3-ad-api sh -c "npm run test:integration:ci"
 | 
			
		||||
  coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
 | 
			
		||||
      when: always
 | 
			
		||||
 | 
			
		||||
###############
 | 
			
		||||
# BUILD STAGE #
 | 
			
		||||
###############
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
  stage: build
 | 
			
		||||
  image: docker:20.10.22
 | 
			
		||||
  variables:
 | 
			
		||||
    DOCKER_TLS_CERTDIR: ''
 | 
			
		||||
  services:
 | 
			
		||||
    - docker:dind
 | 
			
		||||
  before_script:
 | 
			
		||||
    - echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
 | 
			
		||||
  script:
 | 
			
		||||
    - export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
 | 
			
		||||
    - docker pull $CI_REGISTRY_IMAGE:latest || true
 | 
			
		||||
    - >
 | 
			
		||||
      docker build 
 | 
			
		||||
      --pull
 | 
			
		||||
      --cache-from $CI_REGISTRY_IMAGE:latest
 | 
			
		||||
      --tag $CI_REGISTRY_IMAGE:$VERSION
 | 
			
		||||
      --tag $CI_REGISTRY_IMAGE:latest
 | 
			
		||||
      .
 | 
			
		||||
    - docker push $CI_REGISTRY_IMAGE:$VERSION
 | 
			
		||||
    - docker push $CI_REGISTRY_IMAGE:latest
 | 
			
		||||
  only:
 | 
			
		||||
    - main
 | 
			
		||||
| 
						 | 
				
			
			@ -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" ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
# SERVICE
 | 
			
		||||
SERVICE_URL=0.0.0.0
 | 
			
		||||
SERVICE_PORT=5006
 | 
			
		||||
 | 
			
		||||
# PRISMA
 | 
			
		||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
 | 
			
		||||
 | 
			
		||||
# RABBIT MQ
 | 
			
		||||
RMQ_URI=amqp://v3-broker:5672
 | 
			
		||||
 | 
			
		||||
# MESSAGE BROKER
 | 
			
		||||
BROKER_IMAGE=rabbitmq:3-alpine
 | 
			
		||||
 | 
			
		||||
# POSTGRES
 | 
			
		||||
POSTGRES_IMAGE=postgis/postgis:15-3.3
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
###################
 | 
			
		||||
# BUILD FOR CI TESTING
 | 
			
		||||
###################
 | 
			
		||||
 | 
			
		||||
FROM node:18-alpine3.16
 | 
			
		||||
 | 
			
		||||
# Create app directory
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
 | 
			
		||||
COPY package*.json ./
 | 
			
		||||
 | 
			
		||||
# Install app dependencies
 | 
			
		||||
RUN npm ci
 | 
			
		||||
 | 
			
		||||
# Bundle app source
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# Generate prisma client
 | 
			
		||||
RUN npx prisma generate
 | 
			
		||||
 | 
			
		||||
# Create a "dist" folder
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
# Run unit tests
 | 
			
		||||
RUN npm run test:unit:ci
 | 
			
		||||
 | 
			
		||||
# ESLint / Prettier
 | 
			
		||||
RUN npm run lint:check
 | 
			
		||||
RUN npm run pretty:check
 | 
			
		||||
 | 
			
		||||
# Start the server
 | 
			
		||||
CMD [ "node", "dist/main.js" ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
#!/bin/bash
 | 
			
		||||
testlog() {
 | 
			
		||||
	docker logs v3-db | grep -q "database system is ready to accept connections"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
testlog 2> /dev/null
 | 
			
		||||
while [ $? -ne 0 ];
 | 
			
		||||
do
 | 
			
		||||
    sleep 5
 | 
			
		||||
    echo "Waiting for Test DB to be up..."
 | 
			
		||||
	testlog 2> /dev/null
 | 
			
		||||
done
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  v3-ad-api:
 | 
			
		||||
    container_name: v3-ad-api
 | 
			
		||||
    build:
 | 
			
		||||
      dockerfile: ci/Dockerfile
 | 
			
		||||
      context: .
 | 
			
		||||
    env_file:
 | 
			
		||||
      - ci/.env.ci
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5006:5006
 | 
			
		||||
    networks:
 | 
			
		||||
      - v3-network
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  v3-network:
 | 
			
		||||
    name: v3-network
 | 
			
		||||
    external: true
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  db:
 | 
			
		||||
    container_name: v3-db
 | 
			
		||||
    image: ${POSTGRES_IMAGE}
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_DB: mobicoop
 | 
			
		||||
      POSTGRES_USER: mobicoop
 | 
			
		||||
      POSTGRES_PASSWORD: mobicoop
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5432:5432
 | 
			
		||||
    networks:
 | 
			
		||||
      - v3-network
 | 
			
		||||
 | 
			
		||||
  broker:
 | 
			
		||||
    container_name: v3-broker
 | 
			
		||||
    image: ${BROKER_IMAGE}
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5672:5672
 | 
			
		||||
    networks:
 | 
			
		||||
      - v3-network
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  v3-network:
 | 
			
		||||
    name: v3-network
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										55
									
								
								package.json
								
								
								
								
							
							
						
						
									
										55
									
								
								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,41 @@
 | 
			
		|||
    "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-transformer": "^0.5.1",
 | 
			
		||||
    "class-validator": "^0.14.0",
 | 
			
		||||
    "ioredis": "^5.3.2",
 | 
			
		||||
    "reflect-metadata": "^0.1.13",
 | 
			
		||||
    "rxjs": "^7.2.0"
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +67,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 +82,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 +99,15 @@
 | 
			
		|||
    "collectCoverageFrom": [
 | 
			
		||||
      "**/*.(t|j)s"
 | 
			
		||||
    ],
 | 
			
		||||
    "coveragePathIgnorePatterns": [
 | 
			
		||||
      ".controller.ts",
 | 
			
		||||
      ".module.ts",
 | 
			
		||||
      ".request.ts",
 | 
			
		||||
      ".presenter.ts",
 | 
			
		||||
      ".profile.ts",
 | 
			
		||||
      ".exception.ts",
 | 
			
		||||
      "main.ts"
 | 
			
		||||
    ],
 | 
			
		||||
    "coverageDirectory": "../coverage",
 | 
			
		||||
    "testEnvironment": "node"
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
-- CreateEnum
 | 
			
		||||
CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT');
 | 
			
		||||
 | 
			
		||||
-- CreateEnum
 | 
			
		||||
CREATE TYPE "AddressType" AS ENUM ('HOUSE_NUMBER', 'STREET_ADDRESS', 'LOCALITY', 'VENUE', 'OTHER');
 | 
			
		||||
 | 
			
		||||
-- CreateTable
 | 
			
		||||
CREATE TABLE "ad" (
 | 
			
		||||
    "uuid" UUID NOT NULL,
 | 
			
		||||
    "userUuid" UUID NOT NULL,
 | 
			
		||||
    "driver" BOOLEAN,
 | 
			
		||||
    "passenger" BOOLEAN,
 | 
			
		||||
    "frequency" "Frequency" NOT NULL DEFAULT 'RECURRENT',
 | 
			
		||||
    "fromDate" DATE NOT NULL,
 | 
			
		||||
    "toDate" DATE,
 | 
			
		||||
    "monTime" TIMESTAMPTZ,
 | 
			
		||||
    "tueTime" TIMESTAMPTZ,
 | 
			
		||||
    "wedTime" TIMESTAMPTZ,
 | 
			
		||||
    "thuTime" TIMESTAMPTZ,
 | 
			
		||||
    "friTime" TIMESTAMPTZ,
 | 
			
		||||
    "satTime" TIMESTAMPTZ,
 | 
			
		||||
    "sunTime" TIMESTAMPTZ,
 | 
			
		||||
    "monMargin" INTEGER,
 | 
			
		||||
    "tueMargin" INTEGER,
 | 
			
		||||
    "wedMargin" INTEGER,
 | 
			
		||||
    "thuMargin" INTEGER,
 | 
			
		||||
    "friMargin" INTEGER,
 | 
			
		||||
    "satMargin" INTEGER,
 | 
			
		||||
    "sunMargin" INTEGER,
 | 
			
		||||
    "seatsDriver" SMALLINT,
 | 
			
		||||
    "seatsPassenger" SMALLINT,
 | 
			
		||||
    "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,
 | 
			
		||||
    "position" SMALLINT NOT NULL,
 | 
			
		||||
    "lon" DOUBLE PRECISION NOT NULL,
 | 
			
		||||
    "lat" DOUBLE PRECISION NOT NULL,
 | 
			
		||||
    "houseNumber" TEXT,
 | 
			
		||||
    "street" TEXT,
 | 
			
		||||
    "locality" TEXT,
 | 
			
		||||
    "postalCode" TEXT,
 | 
			
		||||
    "country" TEXT,
 | 
			
		||||
    "type" "AddressType" DEFAULT 'OTHER',
 | 
			
		||||
    "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");
 | 
			
		||||
 | 
			
		||||
-- AddForeignKey
 | 
			
		||||
ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
			
		||||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
// 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      Frequency @default(RECURRENT)
 | 
			
		||||
  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
 | 
			
		||||
  addresses      Address[]
 | 
			
		||||
 | 
			
		||||
  @@index([driver])
 | 
			
		||||
  @@index([passenger])
 | 
			
		||||
  @@index([fromDate])
 | 
			
		||||
  @@index([toDate])
 | 
			
		||||
  @@map("ad")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model Address {
 | 
			
		||||
  uuid        String       @id @default(uuid()) @db.Uuid
 | 
			
		||||
  adUuid      String       @db.Uuid
 | 
			
		||||
  position    Int          @db.SmallInt
 | 
			
		||||
  lon         Float
 | 
			
		||||
  lat         Float
 | 
			
		||||
  houseNumber String?
 | 
			
		||||
  street      String?
 | 
			
		||||
  locality    String?
 | 
			
		||||
  postalCode  String?
 | 
			
		||||
  country     String?
 | 
			
		||||
  type        AddressType? @default(OTHER)
 | 
			
		||||
  createdAt   DateTime     @default(now())
 | 
			
		||||
  updatedAt   DateTime     @default(now()) @updatedAt
 | 
			
		||||
  Ad          Ad           @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
 | 
			
		||||
 | 
			
		||||
  @@map("address")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Frequency {
 | 
			
		||||
  PUNCTUAL
 | 
			
		||||
  RECURRENT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum AddressType {
 | 
			
		||||
  HOUSE_NUMBER
 | 
			
		||||
  STREET_ADDRESS
 | 
			
		||||
  LOCALITY
 | 
			
		||||
  VENUE
 | 
			
		||||
  OTHER
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,19 @@
 | 
			
		|||
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';
 | 
			
		||||
import { AutomapperModule } from '@automapper/nestjs';
 | 
			
		||||
import { classes } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [],
 | 
			
		||||
  imports: [
 | 
			
		||||
    ConfigModule.forRoot({ isGlobal: true }),
 | 
			
		||||
    AutomapperModule.forRoot({ strategyInitializer: classes() }),
 | 
			
		||||
    ConfigurationModule,
 | 
			
		||||
    HealthModule,
 | 
			
		||||
    AdModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [],
 | 
			
		||||
  providers: [],
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								src/main.ts
								
								
								
								
							
							
						
						
									
										21
									
								
								src/main.ts
								
								
								
								
							| 
						 | 
				
			
			@ -1,8 +1,27 @@
 | 
			
		|||
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<MicroserviceOptions>({
 | 
			
		||||
    transport: Transport.TCP,
 | 
			
		||||
  });
 | 
			
		||||
  app.connectMicroservice<MicroserviceOptions>({
 | 
			
		||||
    transport: Transport.GRPC,
 | 
			
		||||
    options: {
 | 
			
		||||
      package: ['ad', '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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { AdController } from './adapters/primaries/ad.controller';
 | 
			
		||||
import { DatabaseModule } from '../database/database.module';
 | 
			
		||||
import { CqrsModule } from '@nestjs/cqrs';
 | 
			
		||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { AdProfile } from './mappers/ad.profile';
 | 
			
		||||
import { AdsRepository } from './adapters/secondaries/ads.repository';
 | 
			
		||||
import { Messager } from './adapters/secondaries/messager';
 | 
			
		||||
import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    DatabaseModule,
 | 
			
		||||
    CqrsModule,
 | 
			
		||||
    RabbitMQModule.forRootAsync(RabbitMQModule, {
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      useFactory: async (configService: ConfigService) => ({
 | 
			
		||||
        exchanges: [
 | 
			
		||||
          {
 | 
			
		||||
            name: configService.get<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            type: 'topic',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        uri: configService.get<string>('RMQ_URI'),
 | 
			
		||||
        connectionInitOptions: { wait: false },
 | 
			
		||||
      }),
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [AdController],
 | 
			
		||||
  providers: [AdProfile, AdsRepository, Messager, FindAdByUuidUseCase],
 | 
			
		||||
})
 | 
			
		||||
export class AdModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import { Mapper } from '@automapper/core';
 | 
			
		||||
import { InjectMapper } from '@automapper/nestjs';
 | 
			
		||||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { QueryBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
 | 
			
		||||
import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request';
 | 
			
		||||
import { AdPresenter } from './ad.presenter';
 | 
			
		||||
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
 | 
			
		||||
import { Ad } from '../../domain/entities/ad';
 | 
			
		||||
 | 
			
		||||
@UsePipes(
 | 
			
		||||
  new RpcValidationPipe({
 | 
			
		||||
    whitelist: false,
 | 
			
		||||
    forbidUnknownValues: false,
 | 
			
		||||
  }),
 | 
			
		||||
)
 | 
			
		||||
@Controller()
 | 
			
		||||
export class AdController {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly queryBus: QueryBus,
 | 
			
		||||
    @InjectMapper() private readonly _mapper: Mapper,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod('AdsService', 'FindOneByUuid')
 | 
			
		||||
  async findOnebyUuid(data: FindAdByUuidRequest): Promise<AdPresenter> {
 | 
			
		||||
    try {
 | 
			
		||||
      console.log('ici');
 | 
			
		||||
      const ad = await this.queryBus.execute(new FindAdByUuidQuery(data));
 | 
			
		||||
      return this._mapper.map(ad, Ad, AdPresenter);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw new RpcException({
 | 
			
		||||
        code: e.code,
 | 
			
		||||
        message: e.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
export class AdPresenter {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,86 @@
 | 
			
		|||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
package ad;
 | 
			
		||||
 | 
			
		||||
service AdsService {
 | 
			
		||||
  rpc FindOneByUuid(AdByUuid) returns (Ad);
 | 
			
		||||
  rpc FindAll(AdFilter) returns (Ads);
 | 
			
		||||
  rpc Create(Ad) returns (Ad);
 | 
			
		||||
  rpc Update(Ad) returns (Ad);
 | 
			
		||||
  rpc Delete(AdByUuid) returns (Empty);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message AdByUuid {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Ad {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
  string userUuid = 2;
 | 
			
		||||
  bool driver = 3;
 | 
			
		||||
  bool passenger = 4;
 | 
			
		||||
  int32 frequency = 5;
 | 
			
		||||
  string fromDate = 6;
 | 
			
		||||
  string toDate = 7;
 | 
			
		||||
  Schedule schedule = 8;
 | 
			
		||||
  MarginDurations marginDurations = 9;
 | 
			
		||||
  int32 seatsPassenger = 10;
 | 
			
		||||
  int32 seatsDriver = 11;
 | 
			
		||||
  bool strict = 12;
 | 
			
		||||
  Addresses addresses = 13;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Schedule {
 | 
			
		||||
  string mon = 1;
 | 
			
		||||
  string tue = 2;
 | 
			
		||||
  string wed = 3;
 | 
			
		||||
  string thu = 4;
 | 
			
		||||
  string fri = 5;
 | 
			
		||||
  string sat = 6;
 | 
			
		||||
  string sun = 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message MarginDurations {
 | 
			
		||||
  int32 mon = 1;
 | 
			
		||||
  int32 tue = 2;
 | 
			
		||||
  int32 wed = 3;
 | 
			
		||||
  int32 thu = 4;
 | 
			
		||||
  int32 fri = 5;
 | 
			
		||||
  int32 sat = 6;
 | 
			
		||||
  int32 sun = 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Addresses {
 | 
			
		||||
  repeated Address address = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Address {
 | 
			
		||||
  float lon = 1;
 | 
			
		||||
  float lat = 2;
 | 
			
		||||
  string houseNumber = 3;
 | 
			
		||||
  string street = 4;
 | 
			
		||||
  string locality = 5;
 | 
			
		||||
  string postalCode = 6;
 | 
			
		||||
  string country = 7;
 | 
			
		||||
  AddressType type = 8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum AddressType {
 | 
			
		||||
  HOUSE_NUMBER = 1;
 | 
			
		||||
  STREET_ADDRESS = 2;
 | 
			
		||||
  LOCALITY = 3;
 | 
			
		||||
  VENUE = 4;
 | 
			
		||||
  OTHER = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message AdFilter {
 | 
			
		||||
  optional int32 page = 1;
 | 
			
		||||
  optional int32 perPage = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Ads {
 | 
			
		||||
  repeated Ad data = 1;
 | 
			
		||||
  int32 total = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Empty {}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { AdRepository } from '../../../database/domain/ad-repository';
 | 
			
		||||
import { Ad } from '../../domain/entities/ad';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdsRepository extends AdRepository<Ad> {
 | 
			
		||||
  protected _model = 'ad';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { IMessageBroker } from '../../domain/interfaces/message-broker';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class Messager extends IMessageBroker {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly _amqpConnection: AmqpConnection,
 | 
			
		||||
    configService: ConfigService,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(configService.get<string>('RMQ_EXCHANGE'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  publish(routingKey: string, message: string): void {
 | 
			
		||||
    this._amqpConnection.publish(this.exchange, routingKey, message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class FindAdByUuidRequest {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
export class Ad {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
import { NotFoundException } from '@nestjs/common';
 | 
			
		||||
import { QueryHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
 | 
			
		||||
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
 | 
			
		||||
import { Messager } from '../../adapters/secondaries/messager';
 | 
			
		||||
import { Ad } from '../entities/ad';
 | 
			
		||||
 | 
			
		||||
@QueryHandler(FindAdByUuidQuery)
 | 
			
		||||
export class FindAdByUuidUseCase {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly repository: AdsRepository,
 | 
			
		||||
    private readonly messager: Messager,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(findAdByUuid: FindAdByUuidQuery): Promise<Ad> {
 | 
			
		||||
    try {
 | 
			
		||||
      const ad = await this.repository.findOneByUuid(findAdByUuid.uuid);
 | 
			
		||||
      if (!ad) throw new NotFoundException();
 | 
			
		||||
      return ad;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.messager.publish(
 | 
			
		||||
        'logging.ad.read.warning',
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
          query: findAdByUuid,
 | 
			
		||||
          error,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import { createMap, Mapper } from '@automapper/core';
 | 
			
		||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Ad } from '../domain/entities/ad';
 | 
			
		||||
import { AdPresenter } from '../adapters/primaries/ad.presenter';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdProfile extends AutomapperProfile {
 | 
			
		||||
  constructor(@InjectMapper() mapper: Mapper) {
 | 
			
		||||
    super(mapper);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override get profile() {
 | 
			
		||||
    return (mapper) => {
 | 
			
		||||
      createMap(mapper, Ad, AdPresenter);
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { FindAdByUuidRequest } from '../domain/dtos/find-ad-by-uuid.request';
 | 
			
		||||
 | 
			
		||||
export class FindAdByUuidQuery {
 | 
			
		||||
  readonly uuid: string;
 | 
			
		||||
 | 
			
		||||
  constructor(findAdByUuidRequest: FindAdByUuidRequest) {
 | 
			
		||||
    this.uuid = findAdByUuidRequest.uuid;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string>('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<string>('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<Configuration> = JSON.parse(message);
 | 
			
		||||
    configurations.forEach(async (configuration) => {
 | 
			
		||||
      if (
 | 
			
		||||
        configuration.domain ==
 | 
			
		||||
        this._configService.get<string>('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),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string> {
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { DeleteConfigurationRequest } from '../domain/dtos/delete-configuration.request';
 | 
			
		||||
 | 
			
		||||
export class DeleteConfigurationCommand {
 | 
			
		||||
  readonly deleteConfigurationRequest: DeleteConfigurationRequest;
 | 
			
		||||
 | 
			
		||||
  constructor(deleteConfigurationRequest: DeleteConfigurationRequest) {
 | 
			
		||||
    this.deleteConfigurationRequest = deleteConfigurationRequest;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { SetConfigurationRequest } from '../domain/dtos/set-configuration.request';
 | 
			
		||||
 | 
			
		||||
export class SetConfigurationCommand {
 | 
			
		||||
  readonly setConfigurationRequest: SetConfigurationRequest;
 | 
			
		||||
 | 
			
		||||
  constructor(setConfigurationRequest: SetConfigurationRequest) {
 | 
			
		||||
    this.setConfigurationRequest = setConfigurationRequest;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<RedisModuleOptions> => ({
 | 
			
		||||
        config: {
 | 
			
		||||
          host: configService.get<string>('REDIS_HOST'),
 | 
			
		||||
          port: configService.get<number>('REDIS_PORT'),
 | 
			
		||||
          password: configService.get<string>('REDIS_PASSWORD'),
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
    RabbitMQModule.forRootAsync(RabbitMQModule, {
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
      useFactory: async (
 | 
			
		||||
        configService: ConfigService,
 | 
			
		||||
      ): Promise<RabbitMQConfig> => ({
 | 
			
		||||
        exchanges: [
 | 
			
		||||
          {
 | 
			
		||||
            name: configService.get<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            type: 'topic',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        handlers: {
 | 
			
		||||
          setConfiguration: {
 | 
			
		||||
            exchange: configService.get<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            routingKey: ['configuration.create', 'configuration.update'],
 | 
			
		||||
          },
 | 
			
		||||
          deleteConfiguration: {
 | 
			
		||||
            exchange: configService.get<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            routingKey: 'configuration.delete',
 | 
			
		||||
          },
 | 
			
		||||
          propagateConfiguration: {
 | 
			
		||||
            exchange: configService.get<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            routingKey: 'configuration.propagate',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        uri: configService.get<string>('RMQ_URI'),
 | 
			
		||||
        connectionInitOptions: { wait: false },
 | 
			
		||||
        enableControllerDiscovery: true,
 | 
			
		||||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [ConfigurationMessagerController],
 | 
			
		||||
  providers: [
 | 
			
		||||
    GetConfigurationUseCase,
 | 
			
		||||
    SetConfigurationUseCase,
 | 
			
		||||
    DeleteConfigurationUseCase,
 | 
			
		||||
    RedisConfigurationRepository,
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class ConfigurationModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class DeleteConfigurationRequest {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  domain: string;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  key: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
export class Configuration {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  domain: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  key: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  value: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export abstract class IConfigurationRepository {
 | 
			
		||||
  abstract get(key: string): Promise<string>;
 | 
			
		||||
  abstract set(key: string, value: string): void;
 | 
			
		||||
  abstract del(key: string): void;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string> {
 | 
			
		||||
    return this._configurationRepository.get(
 | 
			
		||||
      getConfigurationQuery.domain + ':' + getConfigurationQuery.key,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
export class GetConfigurationQuery {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
 | 
			
		||||
  constructor(domain: string, key: string) {
 | 
			
		||||
    this.domain = domain;
 | 
			
		||||
    this.key = key;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>(
 | 
			
		||||
      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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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>(
 | 
			
		||||
      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');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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>(
 | 
			
		||||
      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();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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>(
 | 
			
		||||
      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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,259 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
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<T> implements IRepository<T> {
 | 
			
		||||
  protected _model: string;
 | 
			
		||||
 | 
			
		||||
  constructor(protected readonly _prisma: PrismaService) {}
 | 
			
		||||
 | 
			
		||||
  async findAll(
 | 
			
		||||
    page = 1,
 | 
			
		||||
    perPage = 10,
 | 
			
		||||
    where?: any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<ICollection<T>> {
 | 
			
		||||
    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<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this._prisma[this._model].findUnique({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findOne(where: any, include?: any): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this._prisma[this._model].findFirst({
 | 
			
		||||
        where: where,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.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<T> | any, include?: any): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await this._prisma[this._model].create({
 | 
			
		||||
        data: entity,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return res;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async update(uuid: string, entity: Partial<T>): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const updatedEntity = await this._prisma[this._model].update({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
        data: entity,
 | 
			
		||||
      });
 | 
			
		||||
      return updatedEntity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateWhere(
 | 
			
		||||
    where: any,
 | 
			
		||||
    entity: Partial<T> | any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const updatedEntity = await this._prisma[this._model].update({
 | 
			
		||||
        where: where,
 | 
			
		||||
        data: entity,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return updatedEntity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(uuid: string): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this._prisma[this._model].delete({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deleteMany(where: any): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this._prisma[this._model].deleteMany({
 | 
			
		||||
        where: where,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findAllByQuery(
 | 
			
		||||
    include: string[],
 | 
			
		||||
    where: string[],
 | 
			
		||||
  ): Promise<ICollection<T>> {
 | 
			
		||||
    const query = `SELECT ${include.join(',')} FROM ${
 | 
			
		||||
      this._model
 | 
			
		||||
    } WHERE ${where.join(' AND ')}`;
 | 
			
		||||
    const data: T[] = await this._prisma.$queryRawUnsafe(query);
 | 
			
		||||
    return Promise.resolve({
 | 
			
		||||
      data,
 | 
			
		||||
      total: data.length,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createWithFields(fields: object): Promise<number> {
 | 
			
		||||
    try {
 | 
			
		||||
      const command = `INSERT INTO ${this._model} ("${Object.keys(fields).join(
 | 
			
		||||
        '","',
 | 
			
		||||
      )}") VALUES (${Object.values(fields).join(',')})`;
 | 
			
		||||
      return await this._prisma.$executeRawUnsafe(command);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateWithFields(uuid: string, entity: object): Promise<number> {
 | 
			
		||||
    entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
 | 
			
		||||
    const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
 | 
			
		||||
    try {
 | 
			
		||||
      const command = `UPDATE ${this._model} SET ${values.join(
 | 
			
		||||
        ', ',
 | 
			
		||||
      )} WHERE uuid = '${uuid}'`;
 | 
			
		||||
      return await this._prisma.$executeRawUnsafe(command);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async healthCheck(): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this._prisma.$queryRaw`SELECT 1`;
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { PrismaService } from './adapters/secondaries/prisma-service';
 | 
			
		||||
import { AdRepository } from './domain/ad-repository';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  providers: [PrismaService, AdRepository],
 | 
			
		||||
  exports: [PrismaService, AdRepository],
 | 
			
		||||
})
 | 
			
		||||
export class DatabaseModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
 | 
			
		||||
 | 
			
		||||
export class AdRepository<T> extends PrismaRepository<T> {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
export interface ICollection<T> {
 | 
			
		||||
  data: T[];
 | 
			
		||||
  total: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import { ICollection } from './collection.interface';
 | 
			
		||||
 | 
			
		||||
export interface IRepository<T> {
 | 
			
		||||
  findAll(
 | 
			
		||||
    page: number,
 | 
			
		||||
    perPage: number,
 | 
			
		||||
    params?: any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<ICollection<T>>;
 | 
			
		||||
  findOne(where: any, include?: any): Promise<T>;
 | 
			
		||||
  findOneByUuid(uuid: string, include?: any): Promise<T>;
 | 
			
		||||
  create(entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
 | 
			
		||||
  updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  delete(uuid: string): Promise<T>;
 | 
			
		||||
  deleteMany(where: any): Promise<void>;
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,571 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { PrismaService } from '../../adapters/secondaries/prisma-service';
 | 
			
		||||
import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
 | 
			
		||||
import { DatabaseException } from '../../exceptions/database.exception';
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
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<FakeEntity> {
 | 
			
		||||
  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]);
 | 
			
		||||
  }),
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  $queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
 | 
			
		||||
    return Promise.resolve(fakeEntities);
 | 
			
		||||
  }),
 | 
			
		||||
  $executeRawUnsafe: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockResolvedValueOnce(fakeEntityCreated)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Error('an unknown error');
 | 
			
		||||
    })
 | 
			
		||||
    .mockResolvedValueOnce(fakeEntityCreated)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Error('an unknown error');
 | 
			
		||||
    }),
 | 
			
		||||
  $queryRaw: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return true;
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementation(() => {
 | 
			
		||||
      throw new Prisma.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 Prisma.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 Prisma.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 Prisma.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 Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.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 Prisma.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 Prisma.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>(FakePrismaRepository);
 | 
			
		||||
    prisma = module.get<PrismaService>(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('findAllByquery', () => {
 | 
			
		||||
    it('should return an array of entities', async () => {
 | 
			
		||||
      const entities = await fakeRepository.findAllByQuery(
 | 
			
		||||
        ['uuid', 'name'],
 | 
			
		||||
        ['name is not null'],
 | 
			
		||||
      );
 | 
			
		||||
      expect(entities).toStrictEqual({
 | 
			
		||||
        data: fakeEntities,
 | 
			
		||||
        total: fakeEntities.length,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('createWithFields', () => {
 | 
			
		||||
    it('should create an entity', async () => {
 | 
			
		||||
      jest.spyOn(prisma, '$queryRawUnsafe');
 | 
			
		||||
 | 
			
		||||
      const newEntity = await fakeRepository.createWithFields({
 | 
			
		||||
        uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
        name: 'my-name',
 | 
			
		||||
      });
 | 
			
		||||
      expect(newEntity).toBe(fakeEntityCreated);
 | 
			
		||||
      expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.createWithFields({
 | 
			
		||||
          uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.createWithFields({
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('updateWithFields', () => {
 | 
			
		||||
    it('should update an entity', async () => {
 | 
			
		||||
      jest.spyOn(prisma, '$queryRawUnsafe');
 | 
			
		||||
 | 
			
		||||
      const updatedEntity = await fakeRepository.updateWithFields(
 | 
			
		||||
        '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
        {
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(updatedEntity).toBe(fakeEntityCreated);
 | 
			
		||||
      expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWithFields(
 | 
			
		||||
          '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          {
 | 
			
		||||
            name: 'my-name',
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWithFields(
 | 
			
		||||
          '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          {
 | 
			
		||||
            name: 'my-name',
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ).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,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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<HealthCheckResponse> {
 | 
			
		||||
    const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy(
 | 
			
		||||
      'prisma',
 | 
			
		||||
    );
 | 
			
		||||
    return {
 | 
			
		||||
      status:
 | 
			
		||||
        healthCheck['prisma'].status == 'up'
 | 
			
		||||
          ? ServingStatus.SERVING
 | 
			
		||||
          : ServingStatus.NOT_SERVING,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string>('RMQ_EXCHANGE'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  publish(routingKey: string, message: string): void {
 | 
			
		||||
    this._amqpConnection.publish(this.exchange, routingKey, message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<HealthIndicatorResult> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this._repository.healthCheck();
 | 
			
		||||
      return this.getStatus(key, true);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw new HealthCheckError('Prisma', {
 | 
			
		||||
        prisma: e.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string>('RMQ_EXCHANGE'),
 | 
			
		||||
            type: 'topic',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        uri: configService.get<string>('RMQ_URI'),
 | 
			
		||||
        connectionInitOptions: { wait: false },
 | 
			
		||||
      }),
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
    }),
 | 
			
		||||
    DatabaseModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [HealthServerController, HealthController],
 | 
			
		||||
  providers: [PrismaHealthIndicatorUseCase, AdsRepository, Messager],
 | 
			
		||||
})
 | 
			
		||||
export class HealthModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>(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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
 | 
			
		||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
 | 
			
		||||
import { AdsRepository } from '../../../ad/adapters/secondaries/ads.repository';
 | 
			
		||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
 | 
			
		||||
 | 
			
		||||
const mockAdsRepository = {
 | 
			
		||||
  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: AdsRepository,
 | 
			
		||||
          useValue: mockAdsRepository,
 | 
			
		||||
        },
 | 
			
		||||
        PrismaHealthIndicatorUseCase,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
 | 
			
		||||
      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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
import { ArgumentMetadata } from '@nestjs/common';
 | 
			
		||||
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
 | 
			
		||||
import { FindAdByUuidRequest } from '../../../modules/ad/domain/dtos/find-ad-by-uuid.request';
 | 
			
		||||
 | 
			
		||||
describe('RpcValidationPipe', () => {
 | 
			
		||||
  it('should not validate request', async () => {
 | 
			
		||||
    const target: RpcValidationPipe = new RpcValidationPipe({
 | 
			
		||||
      whitelist: true,
 | 
			
		||||
      forbidUnknownValues: false,
 | 
			
		||||
    });
 | 
			
		||||
    const metadata: ArgumentMetadata = {
 | 
			
		||||
      type: 'body',
 | 
			
		||||
      metatype: FindAdByUuidRequest,
 | 
			
		||||
      data: '',
 | 
			
		||||
    };
 | 
			
		||||
    await target.transform(<FindAdByUuidRequest>{}, metadata).catch((err) => {
 | 
			
		||||
      expect(err.message).toEqual('Rpc Exception');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
		Reference in New Issue