Merge branch 'useRedis' into 'main'
Switch to redis See merge request v3/service/configuration!25
This commit is contained in:
		
						commit
						efb35d6d2b
					
				
							
								
								
									
										25
									
								
								.env.dist
								
								
								
								
							
							
						
						
									
										25
									
								
								.env.dist
								
								
								
								
							| 
						 | 
				
			
			@ -3,10 +3,29 @@ SERVICE_URL=0.0.0.0
 | 
			
		|||
SERVICE_PORT=5003
 | 
			
		||||
HEALTH_SERVICE_PORT=6003
 | 
			
		||||
 | 
			
		||||
# PRISMA
 | 
			
		||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration"
 | 
			
		||||
 | 
			
		||||
# MESSAGE BROKER
 | 
			
		||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
 | 
			
		||||
MESSAGE_BROKER_EXCHANGE=mobicoop
 | 
			
		||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
 | 
			
		||||
 | 
			
		||||
# REDIS
 | 
			
		||||
REDIS_HOST=v3-redis
 | 
			
		||||
REDIS_PASSWORD=redis
 | 
			
		||||
REDIS_PORT=6379
 | 
			
		||||
 | 
			
		||||
# DEFAULT CONFIGURATION
 | 
			
		||||
 | 
			
		||||
# CARPOOL
 | 
			
		||||
# default carpool departure time margin (in seconds)
 | 
			
		||||
DEPARTURE_TIME_MARGIN=900
 | 
			
		||||
# default role
 | 
			
		||||
ROLE=passenger
 | 
			
		||||
# seats proposed as driver / requested as passenger
 | 
			
		||||
SEATS_PROPOSED=3
 | 
			
		||||
SEATS_REQUESTED=1
 | 
			
		||||
# accept only same frequency requests
 | 
			
		||||
STRICT_FREQUENCY=false
 | 
			
		||||
 | 
			
		||||
# PAGINATION
 | 
			
		||||
# number of results per page
 | 
			
		||||
PER_PAGE=10
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,5 +2,7 @@
 | 
			
		|||
SERVICE_URL=0.0.0.0
 | 
			
		||||
SERVICE_PORT=5003
 | 
			
		||||
 | 
			
		||||
# PRISMA
 | 
			
		||||
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=configuration"
 | 
			
		||||
# REDIS
 | 
			
		||||
REDIS_HOST=v3-redis-test
 | 
			
		||||
REDIS_PASSWORD=redis
 | 
			
		||||
REDIS_PORT=6380
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ test:
 | 
			
		|||
    - docker-compose -f docker-compose.ci.tools.yml -p configuration-tools --env-file ci/.env.ci up -d
 | 
			
		||||
    - sh ci/wait-up.sh
 | 
			
		||||
    - docker-compose -f docker-compose.ci.service.yml -p configuration-service --env-file ci/.env.ci up -d
 | 
			
		||||
    - docker exec -t v3-configuration-api sh -c "npm run test:integration:ci"
 | 
			
		||||
    # - docker exec -t v3-configuration-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'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,12 +12,8 @@ WORKDIR /usr/src/app
 | 
			
		|||
# 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 . .
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +39,6 @@ COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modul
 | 
			
		|||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +63,6 @@ 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										58
									
								
								README.md
								
								
								
								
							
							
						
						
									
										58
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -1,12 +1,9 @@
 | 
			
		|||
# Mobicoop V3 - Configuration Service
 | 
			
		||||
 | 
			
		||||
Configuration items management. Used to configure all services using a broker to disseminate the configuration items.
 | 
			
		||||
 | 
			
		||||
This service handles the persistence of the configuration items of all services in a database, and sends values _via_ the broker.
 | 
			
		||||
Configuration items management. Used to configure and store all services using redis as database.
 | 
			
		||||
 | 
			
		||||
Each item consists in :
 | 
			
		||||
 | 
			
		||||
-   a **uuid** : a unique identifier for the configuration item
 | 
			
		||||
-   a **domain** : each service is associated with one or more domains, represented by an uppercase string (eg. _USER_)
 | 
			
		||||
-   a **key** : the key of the configuration item (a string)
 | 
			
		||||
-   a **value** : the value of the configuration item (always a string, each service must cast the value if needed)
 | 
			
		||||
| 
						 | 
				
			
			@ -17,9 +14,10 @@ Practically, it's the other way round as it's easier to use this configuration s
 | 
			
		|||
 | 
			
		||||
## Available domains
 | 
			
		||||
 | 
			
		||||
-   **AD** : ad related configuration items
 | 
			
		||||
-   **MATCHER** : matching algotithm related configuration items
 | 
			
		||||
-   **USER** : user related configuration items
 | 
			
		||||
-   **CARPOOL** : carpool related configuration items (eg. default number of seats proposed as a driver)
 | 
			
		||||
-   **PAGINATION** : pagination related configuration items (eg. default number of results per page)
 | 
			
		||||
 | 
			
		||||
New domains will be added in the future depending on the needs !
 | 
			
		||||
 | 
			
		||||
## Requirements
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -57,11 +55,7 @@ A RabbitMQ instance is also required to send / receive messages when data has be
 | 
			
		|||
 | 
			
		||||
## Database migration
 | 
			
		||||
 | 
			
		||||
Before using the app, you need to launch the database migration (it will be launched inside the container) :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run migrate
 | 
			
		||||
```
 | 
			
		||||
Redis database is automatically populated at the start of the service. If keys already exists in the database, items are preserved.
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,52 +65,24 @@ The app exposes the following [gRPC](https://grpc.io/) services :
 | 
			
		|||
 | 
			
		||||
    ```json
 | 
			
		||||
    {
 | 
			
		||||
        "domain": "AD",
 | 
			
		||||
        "domain": "CARPOOL",
 | 
			
		||||
        "key": "seatsProposed"
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
-   **Set** : create or update a configuration item
 | 
			
		||||
-   **Set** : update a configuration item
 | 
			
		||||
 | 
			
		||||
    ```json
 | 
			
		||||
    {
 | 
			
		||||
        "domain": "USER",
 | 
			
		||||
        "key": "key1",
 | 
			
		||||
        "value": "value1"
 | 
			
		||||
        "domain": "CARPOOL",
 | 
			
		||||
        "key": "seatsProposed",
 | 
			
		||||
        "value": "3"
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
-   **Delete** : delete a configuration item by its domain and key
 | 
			
		||||
 | 
			
		||||
    ```json
 | 
			
		||||
    {
 | 
			
		||||
        "domain": "AD",
 | 
			
		||||
        "key": "seatsProposed"
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
-   **Propagate** : propagate all configuration items using the message broker
 | 
			
		||||
 | 
			
		||||
    ```json
 | 
			
		||||
    {}
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
## Messages
 | 
			
		||||
 | 
			
		||||
As mentionned earlier, RabbitMQ messages are sent after these events :
 | 
			
		||||
 | 
			
		||||
-   **Set** (message : the created / updated configuration item informations)
 | 
			
		||||
 | 
			
		||||
-   **Delete** (message : the uuid of the deleted configuration item)
 | 
			
		||||
 | 
			
		||||
-   **Propagate** (message : all the configuration items)
 | 
			
		||||
 | 
			
		||||
Various messages are also sent for logging purpose.
 | 
			
		||||
 | 
			
		||||
## Tests / ESLint / Prettier
 | 
			
		||||
 | 
			
		||||
Tests are run outside the container for ease of use (switching between different environments inside containers using prisma is complicated and error prone).
 | 
			
		||||
The integration tests use a dedicated database (see _db-test_ section of _docker-compose.yml_).
 | 
			
		||||
Tests are run outside the container for ease of use.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# run all tests (unit + integration)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										14
									
								
								ci/.env.ci
								
								
								
								
							
							
						
						
									
										14
									
								
								ci/.env.ci
								
								
								
								
							| 
						 | 
				
			
			@ -2,14 +2,6 @@
 | 
			
		|||
SERVICE_URL=0.0.0.0
 | 
			
		||||
SERVICE_PORT=5003
 | 
			
		||||
 | 
			
		||||
# 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=postgres:15.0
 | 
			
		||||
# REDIS
 | 
			
		||||
REDIS_IMAGE=redis:7.0-alpine
 | 
			
		||||
REDIS_PASSWORD=redis
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,9 +16,6 @@ RUN npm ci
 | 
			
		|||
# Bundle app source
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# Generate prisma client
 | 
			
		||||
RUN npx prisma generate
 | 
			
		||||
 | 
			
		||||
# Create a "dist" folder
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
#!/bin/bash
 | 
			
		||||
testlog() {
 | 
			
		||||
	docker logs v3-db | grep -q "database system is ready to accept connections"
 | 
			
		||||
	docker logs v3-redis | grep -q "Ready to accept connections"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
testlog 2> /dev/null
 | 
			
		||||
while [ $? -ne 0 ];
 | 
			
		||||
do
 | 
			
		||||
    sleep 5
 | 
			
		||||
    echo "Waiting for Test DB to be up..."
 | 
			
		||||
    echo "Waiting for Test Redis to be up..."
 | 
			
		||||
	testlog 2> /dev/null
 | 
			
		||||
done
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,12 @@
 | 
			
		|||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  db:
 | 
			
		||||
    container_name: v3-db
 | 
			
		||||
    image: ${POSTGRES_IMAGE}
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_DB: mobicoop
 | 
			
		||||
      POSTGRES_USER: mobicoop
 | 
			
		||||
      POSTGRES_PASSWORD: mobicoop
 | 
			
		||||
  redis:
 | 
			
		||||
    container_name: v3-redis
 | 
			
		||||
    image: ${REDIS_IMAGE}
 | 
			
		||||
    command: redis-server --requirepass ${REDIS_PASSWORD}
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5432:5432
 | 
			
		||||
    networks:
 | 
			
		||||
      - v3-network
 | 
			
		||||
 | 
			
		||||
  broker:
 | 
			
		||||
    container_name: v3-broker
 | 
			
		||||
    image: ${BROKER_IMAGE}
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5672:5672
 | 
			
		||||
      - 6379:6379
 | 
			
		||||
    networks:
 | 
			
		||||
      - v3-network
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "@mobicoop/configuration",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "@mobicoop/configuration",
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "license": "AGPL",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@grpc/grpc-js": "^1.9.6",
 | 
			
		||||
| 
						 | 
				
			
			@ -23,8 +23,10 @@
 | 
			
		|||
        "@nestjs/platform-express": "^10.2.7",
 | 
			
		||||
        "@nestjs/terminus": "^10.1.1",
 | 
			
		||||
        "@prisma/client": "^5.4.2",
 | 
			
		||||
        "@songkeys/nestjs-redis": "^10.0.0",
 | 
			
		||||
        "class-transformer": "^0.5.1",
 | 
			
		||||
        "class-validator": "^0.14.0",
 | 
			
		||||
        "ioredis": "^5.3.2",
 | 
			
		||||
        "reflect-metadata": "^0.1.13",
 | 
			
		||||
        "rimraf": "^5.0.5",
 | 
			
		||||
        "rxjs": "^7.8.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -1096,6 +1098,11 @@
 | 
			
		|||
      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@ioredis/commands": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@isaacs/cliui": {
 | 
			
		||||
      "version": "8.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -2490,6 +2497,27 @@
 | 
			
		|||
        "@sinonjs/commons": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@songkeys/nestjs-redis": {
 | 
			
		||||
      "version": "10.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@songkeys/nestjs-redis/-/nestjs-redis-10.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-s56+NECuJXzcaPLYzpvA2xjL0e/1Zy55UE0q6b1UqpbQSKI06TFPFCWCMUadJigiuB26O1hxi+lmDbzahKvcLg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tslib": "2.6.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@nestjs/common": "^10.0.0",
 | 
			
		||||
        "@nestjs/core": "^10.0.0",
 | 
			
		||||
        "ioredis": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@songkeys/nestjs-redis/node_modules/tslib": {
 | 
			
		||||
      "version": "2.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tsconfig/node10": {
 | 
			
		||||
      "version": "1.0.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4098,6 +4126,14 @@
 | 
			
		|||
        "node": ">=0.8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cluster-key-slot": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/co": {
 | 
			
		||||
      "version": "4.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4581,6 +4617,14 @@
 | 
			
		|||
        "node": ">=0.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/denque": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/depd": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -5559,20 +5603,6 @@
 | 
			
		|||
      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fsevents": {
 | 
			
		||||
      "version": "2.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/function-bind": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -5952,6 +5982,29 @@
 | 
			
		|||
        "node": ">= 0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ioredis": {
 | 
			
		||||
      "version": "5.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@ioredis/commands": "^1.1.1",
 | 
			
		||||
        "cluster-key-slot": "^1.1.0",
 | 
			
		||||
        "debug": "^4.3.4",
 | 
			
		||||
        "denque": "^2.1.0",
 | 
			
		||||
        "lodash.defaults": "^4.2.0",
 | 
			
		||||
        "lodash.isarguments": "^3.1.0",
 | 
			
		||||
        "redis-errors": "^1.2.0",
 | 
			
		||||
        "redis-parser": "^3.0.0",
 | 
			
		||||
        "standard-as-callback": "^2.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12.22.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "opencollective",
 | 
			
		||||
        "url": "https://opencollective.com/ioredis"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ipaddr.js": {
 | 
			
		||||
      "version": "1.9.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -6979,6 +7032,16 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash.defaults": {
 | 
			
		||||
      "version": "4.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash.isarguments": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash.memoize": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -8022,6 +8085,25 @@
 | 
			
		|||
        "node": ">= 0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/redis-errors": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/redis-parser": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "redis-errors": "^1.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/reflect-metadata": {
 | 
			
		||||
      "version": "0.1.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -8546,6 +8628,11 @@
 | 
			
		|||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/standard-as-callback": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/statuses": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								package.json
								
								
								
								
							
							
						
						
									
										19
									
								
								package.json
								
								
								
								
							| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "@mobicoop/configuration",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "description": "Mobicoop V3 Configuration Service",
 | 
			
		||||
  "author": "sbriat",
 | 
			
		||||
  "private": true,
 | 
			
		||||
| 
						 | 
				
			
			@ -17,18 +17,11 @@
 | 
			
		|||
    "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": "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-configuration-api sh -c 'npx prisma generate'",
 | 
			
		||||
    "migrate": "docker exec v3-configuration-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"
 | 
			
		||||
    "test:e2e": "jest --config ./test/jest-e2e.json"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@grpc/grpc-js": "^1.9.6",
 | 
			
		||||
| 
						 | 
				
			
			@ -44,9 +37,10 @@
 | 
			
		|||
    "@nestjs/microservices": "^10.2.7",
 | 
			
		||||
    "@nestjs/platform-express": "^10.2.7",
 | 
			
		||||
    "@nestjs/terminus": "^10.1.1",
 | 
			
		||||
    "@prisma/client": "^5.4.2",
 | 
			
		||||
    "@songkeys/nestjs-redis": "^10.0.0",
 | 
			
		||||
    "class-transformer": "^0.5.1",
 | 
			
		||||
    "class-validator": "^0.14.0",
 | 
			
		||||
    "ioredis": "^5.3.2",
 | 
			
		||||
    "reflect-metadata": "^0.1.13",
 | 
			
		||||
    "rimraf": "^5.0.5",
 | 
			
		||||
    "rxjs": "^7.8.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +63,6 @@
 | 
			
		|||
    "eslint-plugin-prettier": "^5.0.1",
 | 
			
		||||
    "jest": "29.7.0",
 | 
			
		||||
    "prettier": "^3.0.3",
 | 
			
		||||
    "prisma": "^5.4.2",
 | 
			
		||||
    "source-map-support": "^0.5.21",
 | 
			
		||||
    "supertest": "^6.3.3",
 | 
			
		||||
    "ts-jest": "29.1.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +84,7 @@
 | 
			
		|||
      ".di-tokens.ts",
 | 
			
		||||
      ".response.ts",
 | 
			
		||||
      ".port.ts",
 | 
			
		||||
      ".config.ts",
 | 
			
		||||
      "prisma.service.ts",
 | 
			
		||||
      "main.ts"
 | 
			
		||||
    ],
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +103,7 @@
 | 
			
		|||
      ".di-tokens.ts",
 | 
			
		||||
      ".response.ts",
 | 
			
		||||
      ".port.ts",
 | 
			
		||||
      ".config.ts",
 | 
			
		||||
      "prisma.service.ts",
 | 
			
		||||
      "main.ts"
 | 
			
		||||
    ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
-- CreateTable
 | 
			
		||||
CREATE TABLE "configuration" (
 | 
			
		||||
    "uuid" UUID NOT NULL,
 | 
			
		||||
    "domain" TEXT NOT NULL,
 | 
			
		||||
    "key" TEXT NOT NULL,
 | 
			
		||||
    "value" TEXT NOT NULL,
 | 
			
		||||
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    "updatedAt" TIMESTAMP(3) NOT NULL,
 | 
			
		||||
 | 
			
		||||
    CONSTRAINT "configuration_pkey" PRIMARY KEY ("uuid")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE UNIQUE INDEX "configuration_domain_key_key" ON "configuration"("domain", "key");
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
# Please do not edit this file manually
 | 
			
		||||
# It should be added in your version-control system (i.e. Git)
 | 
			
		||||
provider = "postgresql"
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +0,0 @@
 | 
			
		|||
// This is your Prisma schema file,
 | 
			
		||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
 | 
			
		||||
 | 
			
		||||
generator client {
 | 
			
		||||
  provider      = "prisma-client-js"
 | 
			
		||||
  binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
datasource db {
 | 
			
		||||
  provider = "postgresql"
 | 
			
		||||
  url      = env("DATABASE_URL")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model Configuration {
 | 
			
		||||
  uuid      String   @id @default(uuid()) @db.Uuid
 | 
			
		||||
  domain    String
 | 
			
		||||
  key       String
 | 
			
		||||
  value     String
 | 
			
		||||
  createdAt DateTime @default(now())
 | 
			
		||||
  updatedAt DateTime @updatedAt
 | 
			
		||||
 | 
			
		||||
  @@unique([domain, key])
 | 
			
		||||
  @@map("configuration")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,12 +5,6 @@ export const SERVICE_NAME = 'configuration';
 | 
			
		|||
export const GRPC_PACKAGE_NAME = 'configuration';
 | 
			
		||||
export const GRPC_SERVICE_NAME = 'ConfigurationService';
 | 
			
		||||
 | 
			
		||||
// messaging
 | 
			
		||||
export const CONFIGURATION_SET_ROUTING_KEY = 'configuration.set';
 | 
			
		||||
export const CONFIGURATION_DELETED_ROUTING_KEY = 'configuration.deleted';
 | 
			
		||||
export const CONFIGURATION_PROPAGATED_ROUTING_KEY = 'configuration.propagated';
 | 
			
		||||
 | 
			
		||||
// health
 | 
			
		||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
 | 
			
		||||
export const HEALTH_CONFIGURATION_REPOSITORY = 'ConfigurationRepository';
 | 
			
		||||
export const HEALTH_CRITICAL_LOGGING_KEY = 'logging.configuration.health.crit';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,44 +1,69 @@
 | 
			
		|||
import {
 | 
			
		||||
  HealthModule,
 | 
			
		||||
  HealthModuleOptions,
 | 
			
		||||
  HealthRepositoryPort,
 | 
			
		||||
} from '@mobicoop/health-module';
 | 
			
		||||
import { HealthModule, HealthModuleOptions } from '@mobicoop/health-module';
 | 
			
		||||
import { MessagerModule } from '@modules/messager/messager.module';
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { ConfigModule } from '@nestjs/config';
 | 
			
		||||
import {
 | 
			
		||||
  HEALTH_CONFIGURATION_REPOSITORY,
 | 
			
		||||
  HEALTH_CRITICAL_LOGGING_KEY,
 | 
			
		||||
  SERVICE_NAME,
 | 
			
		||||
} from './app.constants';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
 | 
			
		||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationModule } from '@modules/configuration/configuration.module';
 | 
			
		||||
import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
import brokerConfig from './config/broker.config';
 | 
			
		||||
import carpoolConfig from './config/carpool.config';
 | 
			
		||||
import paginationConfig from './config/pagination.config';
 | 
			
		||||
import serviceConfig from './config/service.config';
 | 
			
		||||
import { RedisModule, RedisModuleOptions } from '@songkeys/nestjs-redis';
 | 
			
		||||
import redisConfig from './config/redis.config';
 | 
			
		||||
import { Transport } from '@nestjs/microservices';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    ConfigModule.forRoot({ isGlobal: true }),
 | 
			
		||||
    ConfigModule.forRoot({
 | 
			
		||||
      isGlobal: true,
 | 
			
		||||
      load: [
 | 
			
		||||
        brokerConfig,
 | 
			
		||||
        carpoolConfig,
 | 
			
		||||
        paginationConfig,
 | 
			
		||||
        redisConfig,
 | 
			
		||||
        serviceConfig,
 | 
			
		||||
      ],
 | 
			
		||||
    }),
 | 
			
		||||
    EventEmitterModule.forRoot(),
 | 
			
		||||
    HealthModule.forRootAsync({
 | 
			
		||||
      imports: [ConfigurationModule, MessagerModule],
 | 
			
		||||
      inject: [CONFIGURATION_REPOSITORY, MESSAGE_PUBLISHER],
 | 
			
		||||
      imports: [MessagerModule, ConfigModule],
 | 
			
		||||
      inject: [MESSAGE_PUBLISHER, ConfigService],
 | 
			
		||||
      useFactory: async (
 | 
			
		||||
        configurationRepository: HealthRepositoryPort,
 | 
			
		||||
        messagePublisher: MessagePublisherPort,
 | 
			
		||||
        configService: ConfigService,
 | 
			
		||||
      ): Promise<HealthModuleOptions> => ({
 | 
			
		||||
        serviceName: SERVICE_NAME,
 | 
			
		||||
        criticalLoggingKey: HEALTH_CRITICAL_LOGGING_KEY,
 | 
			
		||||
        checkRepositories: [
 | 
			
		||||
        messagePublisher,
 | 
			
		||||
        checkMicroservices: [
 | 
			
		||||
          {
 | 
			
		||||
            name: HEALTH_CONFIGURATION_REPOSITORY,
 | 
			
		||||
            repository: configurationRepository,
 | 
			
		||||
            host: configService.get<string>('redis.host') as string,
 | 
			
		||||
            port: configService.get<string>('redis.port') as string,
 | 
			
		||||
            password: configService.get<string>('redis.password'),
 | 
			
		||||
            name: 'Redis',
 | 
			
		||||
            transport: Transport.REDIS,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        messagePublisher,
 | 
			
		||||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
    RedisModule.forRootAsync({
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
      useFactory: async (
 | 
			
		||||
        configService: ConfigService,
 | 
			
		||||
      ): Promise<RedisModuleOptions> => {
 | 
			
		||||
        return {
 | 
			
		||||
          config: {
 | 
			
		||||
            host: configService.get<string>('redis.host') as string,
 | 
			
		||||
            port: configService.get<number>('redis.port') as number,
 | 
			
		||||
            password: configService.get<string>('redis.password'),
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    ConfigurationModule,
 | 
			
		||||
    MessagerModule,
 | 
			
		||||
  ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { registerAs } from '@nestjs/config';
 | 
			
		||||
 | 
			
		||||
export default registerAs('broker', () => ({
 | 
			
		||||
  uri: process.env.MESSAGE_BROKER_URI ?? 'amqp://v3-broker:5672',
 | 
			
		||||
  exchange: process.env.MESSAGE_BROKER_EXCHANGE ?? 'mobicoop',
 | 
			
		||||
  durability: process.env.MESSAGE_BROKER_EXCHANGE_DURABILITY
 | 
			
		||||
    ? process.env.MESSAGE_BROKER_EXCHANGE_DURABILITY === 'false'
 | 
			
		||||
      ? false
 | 
			
		||||
      : true
 | 
			
		||||
    : true,
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
import { registerAs } from '@nestjs/config';
 | 
			
		||||
 | 
			
		||||
export default registerAs('carpool', () => ({
 | 
			
		||||
  departureTimeMargin: process.env.DEPARTURE_TIME_MARGIN
 | 
			
		||||
    ? parseInt(process.env.DEPARTURE_TIME_MARGIN, 10)
 | 
			
		||||
    : 900,
 | 
			
		||||
  role: process.env.ROLE ?? 'passenger',
 | 
			
		||||
  seatsProposed: process.env.SEATS_PROPOSED
 | 
			
		||||
    ? parseInt(process.env.SEATS_PROPOSED, 10)
 | 
			
		||||
    : 3,
 | 
			
		||||
  seatsRequested: process.env.SEATS_REQUESTED
 | 
			
		||||
    ? parseInt(process.env.SEATS_REQUESTED, 10)
 | 
			
		||||
    : 1,
 | 
			
		||||
  strictFrequency: process.env.STRICT_FREQUENCY
 | 
			
		||||
    ? process.env.STRICT_FREQUENCY === 'false'
 | 
			
		||||
      ? false
 | 
			
		||||
      : true
 | 
			
		||||
    : false,
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import { registerAs } from '@nestjs/config';
 | 
			
		||||
 | 
			
		||||
export default registerAs('pagination', () => ({
 | 
			
		||||
  perPage: process.env.PER_PAGE ? parseInt(process.env.PER_PAGE, 10) : 10,
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { registerAs } from '@nestjs/config';
 | 
			
		||||
 | 
			
		||||
export default registerAs('redis', () => ({
 | 
			
		||||
  host: process.env.REDIS_HOST ?? 'v3-redis',
 | 
			
		||||
  port: process.env.PORT ? parseInt(process.env.PORT, 10) : 6379,
 | 
			
		||||
  password: process.env.REDIS_PASSWORD ?? 'redis',
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { registerAs } from '@nestjs/config';
 | 
			
		||||
 | 
			
		||||
export default registerAs('service', () => ({
 | 
			
		||||
  url: process.env.SERVICE_URL ?? '0.0.0.0',
 | 
			
		||||
  port: process.env.SERVICE_PORT
 | 
			
		||||
    ? parseInt(process.env.SERVICE_PORT, 10)
 | 
			
		||||
    : 5003,
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1 @@
 | 
			
		|||
export const CONFIGURATION_MESSAGE_PUBLISHER = Symbol(
 | 
			
		||||
  'CONFIGURATION_MESSAGE_PUBLISHER',
 | 
			
		||||
);
 | 
			
		||||
export const CONFIGURATION_REPOSITORY = Symbol('CONFIGURATION_REPOSITORY');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,13 @@
 | 
			
		|||
import { Mapper } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigurationEntity } from './core/domain/configuration.entity';
 | 
			
		||||
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
 | 
			
		||||
import { ConfigurationDomain } from './core/domain/configuration.types';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigurationReadModel,
 | 
			
		||||
  ConfigurationWriteModel,
 | 
			
		||||
} from './infrastructure/configuration.repository';
 | 
			
		||||
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Mapper constructs objects that are used in different layers:
 | 
			
		||||
 * Record is an object that is stored in a database,
 | 
			
		||||
 * Entity is an object that is used in application domain layer,
 | 
			
		||||
 * and a ResponseDTO is an object returned to a user (usually as json).
 | 
			
		||||
 */
 | 
			
		||||
import { v4 } from 'uuid';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ConfigurationMapper
 | 
			
		||||
| 
						 | 
				
			
			@ -27,9 +22,7 @@ export class ConfigurationMapper
 | 
			
		|||
  toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
 | 
			
		||||
    const copy = entity.getProps();
 | 
			
		||||
    const record: ConfigurationWriteModel = {
 | 
			
		||||
      uuid: entity.id,
 | 
			
		||||
      domain: copy.domain,
 | 
			
		||||
      key: copy.key,
 | 
			
		||||
      key: `${copy.identifier.domain}:${copy.identifier.key}`,
 | 
			
		||||
      value: copy.value,
 | 
			
		||||
    };
 | 
			
		||||
    return record;
 | 
			
		||||
| 
						 | 
				
			
			@ -37,12 +30,14 @@ export class ConfigurationMapper
 | 
			
		|||
 | 
			
		||||
  toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
 | 
			
		||||
    const entity = new ConfigurationEntity({
 | 
			
		||||
      id: record.uuid,
 | 
			
		||||
      createdAt: new Date(record.createdAt),
 | 
			
		||||
      updatedAt: new Date(record.updatedAt),
 | 
			
		||||
      id: v4(),
 | 
			
		||||
      createdAt: new Date(),
 | 
			
		||||
      updatedAt: new Date(),
 | 
			
		||||
      props: {
 | 
			
		||||
        domain: record.domain,
 | 
			
		||||
        key: record.key,
 | 
			
		||||
        identifier: {
 | 
			
		||||
          domain: record.key.split(':')[0] as ConfigurationDomain,
 | 
			
		||||
          key: record.key.split(':')[1],
 | 
			
		||||
        },
 | 
			
		||||
        value: record.value,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -52,8 +47,8 @@ export class ConfigurationMapper
 | 
			
		|||
  toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
 | 
			
		||||
    const props = entity.getProps();
 | 
			
		||||
    const response = new ConfigurationResponseDto(entity);
 | 
			
		||||
    response.domain = props.domain;
 | 
			
		||||
    response.key = props.key;
 | 
			
		||||
    response.domain = props.identifier.domain;
 | 
			
		||||
    response.key = props.identifier.key;
 | 
			
		||||
    response.value = props.value;
 | 
			
		||||
    return response;
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,45 +2,26 @@ import { Module, Provider } from '@nestjs/common';
 | 
			
		|||
import { CqrsModule } from '@nestjs/cqrs';
 | 
			
		||||
import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller';
 | 
			
		||||
import { SetConfigurationGrpcController } from './interface/grpc-controllers/set-configuration.grpc.controller';
 | 
			
		||||
import { DeleteConfigurationGrpcController } from './interface/grpc-controllers/delete-configuration.grpc.controller';
 | 
			
		||||
import { PropagateConfigurationsGrpcController } from './interface/grpc-controllers/propagate-configurations.grpc.controller';
 | 
			
		||||
import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler';
 | 
			
		||||
import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler';
 | 
			
		||||
import { SetConfigurationService } from './core/application/commands/set-configuration/set-configuration.service';
 | 
			
		||||
import { DeleteConfigurationService } from './core/application/commands/delete-configuration/delete-configuration.service';
 | 
			
		||||
import { GetConfigurationQueryHandler } from './core/application/queries/get-configuration/get-configuration.query-handler';
 | 
			
		||||
import { ConfigurationMapper } from './configuration.mapper';
 | 
			
		||||
import {
 | 
			
		||||
  CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
  CONFIGURATION_REPOSITORY,
 | 
			
		||||
} from './configuration.di-tokens';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens';
 | 
			
		||||
import { PopulateService } from './core/application/services/populate.service';
 | 
			
		||||
import { ConfigurationRepository } from './infrastructure/configuration.repository';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { PrismaService } from './infrastructure/prisma.service';
 | 
			
		||||
import { PropagateConfigurationsService } from './core/application/commands/propagate-configurations/propagate-configurations.service';
 | 
			
		||||
 | 
			
		||||
const grpcControllers = [
 | 
			
		||||
  GetConfigurationGrpcController,
 | 
			
		||||
  SetConfigurationGrpcController,
 | 
			
		||||
  DeleteConfigurationGrpcController,
 | 
			
		||||
  PropagateConfigurationsGrpcController,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const eventHandlers: Provider[] = [
 | 
			
		||||
  PublishMessageWhenConfigurationIsSetDomainEventHandler,
 | 
			
		||||
  PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const commandHandlers: Provider[] = [
 | 
			
		||||
  SetConfigurationService,
 | 
			
		||||
  DeleteConfigurationService,
 | 
			
		||||
  PropagateConfigurationsService,
 | 
			
		||||
];
 | 
			
		||||
const commandHandlers: Provider[] = [SetConfigurationService];
 | 
			
		||||
 | 
			
		||||
const queryHandlers: Provider[] = [GetConfigurationQueryHandler];
 | 
			
		||||
 | 
			
		||||
const mappers: Provider[] = [ConfigurationMapper];
 | 
			
		||||
 | 
			
		||||
const providers: Provider[] = [PopulateService];
 | 
			
		||||
 | 
			
		||||
const repositories: Provider[] = [
 | 
			
		||||
  {
 | 
			
		||||
    provide: CONFIGURATION_REPOSITORY,
 | 
			
		||||
| 
						 | 
				
			
			@ -48,26 +29,16 @@ const repositories: Provider[] = [
 | 
			
		|||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const messagePublishers: Provider[] = [
 | 
			
		||||
  {
 | 
			
		||||
    provide: CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
    useExisting: MessageBrokerPublisher,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
const orms: Provider[] = [PrismaService];
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [CqrsModule],
 | 
			
		||||
  controllers: [...grpcControllers],
 | 
			
		||||
  providers: [
 | 
			
		||||
    ...eventHandlers,
 | 
			
		||||
    ...commandHandlers,
 | 
			
		||||
    ...queryHandlers,
 | 
			
		||||
    ...mappers,
 | 
			
		||||
    ...providers,
 | 
			
		||||
    ...repositories,
 | 
			
		||||
    ...messagePublishers,
 | 
			
		||||
    ...orms,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [PrismaService, ConfigurationMapper, CONFIGURATION_REPOSITORY],
 | 
			
		||||
  exports: [ConfigurationMapper, CONFIGURATION_REPOSITORY],
 | 
			
		||||
})
 | 
			
		||||
export class ConfigurationModule {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class DeleteConfigurationCommand extends Command {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: CommandProps<DeleteConfigurationCommand>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.domain = props.domain;
 | 
			
		||||
    this.key = props.key;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { DeleteConfigurationCommand } from './delete-configuration.command';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(DeleteConfigurationCommand)
 | 
			
		||||
export class DeleteConfigurationService implements ICommandHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_REPOSITORY)
 | 
			
		||||
    private readonly configurationRepository: ConfigurationRepositoryPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(command: DeleteConfigurationCommand): Promise<boolean> {
 | 
			
		||||
    const configuration: ConfigurationEntity =
 | 
			
		||||
      await this.configurationRepository.findOne({
 | 
			
		||||
        domain: command.domain,
 | 
			
		||||
        key: command.key,
 | 
			
		||||
      });
 | 
			
		||||
    configuration.delete();
 | 
			
		||||
    const isDeleted: boolean =
 | 
			
		||||
      await this.configurationRepository.delete(configuration);
 | 
			
		||||
    return isDeleted;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class PropagateConfigurationsCommand extends Command {
 | 
			
		||||
  constructor(props: CommandProps<PropagateConfigurationsCommand>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import {
 | 
			
		||||
  CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
  CONFIGURATION_REPOSITORY,
 | 
			
		||||
} from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
 | 
			
		||||
import { PropagateConfigurationsCommand } from './propagate-configurations.command';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CONFIGURATION_PROPAGATED_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(PropagateConfigurationsCommand)
 | 
			
		||||
export class PropagateConfigurationsService implements ICommandHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_REPOSITORY)
 | 
			
		||||
    private readonly repository: ConfigurationRepositoryPort,
 | 
			
		||||
    @Inject(CONFIGURATION_MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: MessagePublisherPort,
 | 
			
		||||
    private readonly configurationMapper: ConfigurationMapper,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(): Promise<void> {
 | 
			
		||||
    const configurationItems: ConfigurationEntity[] =
 | 
			
		||||
      await this.repository.findAll({});
 | 
			
		||||
    this.messagePublisher.publish(
 | 
			
		||||
      CONFIGURATION_PROPAGATED_ROUTING_KEY,
 | 
			
		||||
      JSON.stringify(
 | 
			
		||||
        configurationItems.map((configuration: ConfigurationEntity) =>
 | 
			
		||||
          this.configurationMapper.toResponse(configuration),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,42 +1,24 @@
 | 
			
		|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { SetConfigurationCommand } from './set-configuration.command';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(SetConfigurationCommand)
 | 
			
		||||
export class SetConfigurationService implements ICommandHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_REPOSITORY)
 | 
			
		||||
    private readonly repository: ConfigurationRepositoryPort,
 | 
			
		||||
    private readonly configurationRepository: ConfigurationRepositoryPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(command: SetConfigurationCommand): Promise<AggregateID> {
 | 
			
		||||
    try {
 | 
			
		||||
      const existingConfiguration: ConfigurationEntity =
 | 
			
		||||
        await this.repository.findOne({
 | 
			
		||||
          domain: command.domain,
 | 
			
		||||
  async execute(command: SetConfigurationCommand): Promise<void> {
 | 
			
		||||
    await this.configurationRepository.set(
 | 
			
		||||
      {
 | 
			
		||||
        domain: command.domain as ConfigurationDomain,
 | 
			
		||||
        key: command.key,
 | 
			
		||||
        });
 | 
			
		||||
      existingConfiguration.update(command);
 | 
			
		||||
      await this.repository.update(
 | 
			
		||||
        existingConfiguration.id,
 | 
			
		||||
        existingConfiguration,
 | 
			
		||||
      },
 | 
			
		||||
      command.value,
 | 
			
		||||
    );
 | 
			
		||||
      return existingConfiguration.id;
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      if (error instanceof NotFoundException) {
 | 
			
		||||
        try {
 | 
			
		||||
          const newConfiguration = ConfigurationEntity.create(command);
 | 
			
		||||
          await this.repository.insert(newConfiguration);
 | 
			
		||||
          return newConfiguration.id;
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { OnEvent } from '@nestjs/event-emitter';
 | 
			
		||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationDeletedDomainEvent } from '../../domain/events/configuration-deleted.domain-event';
 | 
			
		||||
import { ConfigurationDeletedIntegrationEvent } from '../events/configuration-deleted.integration-event';
 | 
			
		||||
import { CONFIGURATION_DELETED_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PublishMessageWhenConfigurationIsDeletedDomainEventHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: MessagePublisherPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @OnEvent(ConfigurationDeletedDomainEvent.name, {
 | 
			
		||||
    async: true,
 | 
			
		||||
    promisify: true,
 | 
			
		||||
  })
 | 
			
		||||
  async handle(event: ConfigurationDeletedDomainEvent): Promise<any> {
 | 
			
		||||
    const configurationDeletedIntegrationEvent =
 | 
			
		||||
      new ConfigurationDeletedIntegrationEvent({
 | 
			
		||||
        id: event.aggregateId,
 | 
			
		||||
        domain: event.domain,
 | 
			
		||||
        key: event.key,
 | 
			
		||||
        metadata: event.metadata,
 | 
			
		||||
      });
 | 
			
		||||
    this.messagePublisher.publish(
 | 
			
		||||
      CONFIGURATION_DELETED_ROUTING_KEY,
 | 
			
		||||
      JSON.stringify(configurationDeletedIntegrationEvent),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { OnEvent } from '@nestjs/event-emitter';
 | 
			
		||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ConfigurationSetIntegrationEvent } from '../events/configuration-set.integration-event';
 | 
			
		||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationSetDomainEvent } from '../../domain/events/configuration-set.domain-event';
 | 
			
		||||
import { CONFIGURATION_SET_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PublishMessageWhenConfigurationIsSetDomainEventHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: MessagePublisherPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @OnEvent(ConfigurationSetDomainEvent.name, { async: true, promisify: true })
 | 
			
		||||
  async handle(event: ConfigurationSetDomainEvent): Promise<any> {
 | 
			
		||||
    const configurationSetIntegrationEvent =
 | 
			
		||||
      new ConfigurationSetIntegrationEvent({
 | 
			
		||||
        id: event.aggregateId,
 | 
			
		||||
        domain: event.domain,
 | 
			
		||||
        key: event.key,
 | 
			
		||||
        value: event.value,
 | 
			
		||||
        metadata: event.metadata,
 | 
			
		||||
      });
 | 
			
		||||
    this.messagePublisher.publish(
 | 
			
		||||
      CONFIGURATION_SET_ROUTING_KEY,
 | 
			
		||||
      JSON.stringify(configurationSetIntegrationEvent),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class ConfigurationDeletedIntegrationEvent extends IntegrationEvent {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    props: IntegrationEventProps<ConfigurationDeletedIntegrationEvent>,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.domain = props.domain;
 | 
			
		||||
    this.key = props.key;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class ConfigurationSetIntegrationEvent extends IntegrationEvent {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
  readonly value: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: IntegrationEventProps<ConfigurationSetIntegrationEvent>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.domain = props.domain;
 | 
			
		||||
    this.key = props.key;
 | 
			
		||||
    this.value = props.value;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,13 @@
 | 
			
		|||
import { RepositoryPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ConfigurationEntity } from '../../domain/configuration.entity';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigurationIdentifier,
 | 
			
		||||
  ConfigurationValue,
 | 
			
		||||
} from '../../domain/configuration.types';
 | 
			
		||||
 | 
			
		||||
export type ConfigurationRepositoryPort = RepositoryPort<ConfigurationEntity>;
 | 
			
		||||
export interface ConfigurationRepositoryPort {
 | 
			
		||||
  get(identifier: ConfigurationIdentifier): Promise<ConfigurationEntity>;
 | 
			
		||||
  set(
 | 
			
		||||
    identifier: ConfigurationIdentifier,
 | 
			
		||||
    value: ConfigurationValue,
 | 
			
		||||
  ): Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,9 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
 | 
			
		|||
import { GetConfigurationQuery } from './get-configuration.query';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
 | 
			
		||||
@QueryHandler(GetConfigurationQuery)
 | 
			
		||||
export class GetConfigurationQueryHandler implements IQueryHandler {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +13,9 @@ export class GetConfigurationQueryHandler implements IQueryHandler {
 | 
			
		|||
    private readonly configurationRepository: ConfigurationRepositoryPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
  async execute(query: GetConfigurationQuery): Promise<ConfigurationEntity> {
 | 
			
		||||
    return await this.configurationRepository.findOne(query);
 | 
			
		||||
    return await this.configurationRepository.get({
 | 
			
		||||
      domain: query.domain as ConfigurationDomain,
 | 
			
		||||
      key: query.key,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../ports/configuration.repository.port';
 | 
			
		||||
import {
 | 
			
		||||
  CarpoolConfig,
 | 
			
		||||
  ConfigurationDomain,
 | 
			
		||||
  PaginationConfig,
 | 
			
		||||
} from '../../domain/configuration.types';
 | 
			
		||||
import { NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PopulateService implements OnApplicationBootstrap {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(CONFIGURATION_REPOSITORY)
 | 
			
		||||
    private readonly configurationRepository: ConfigurationRepositoryPort,
 | 
			
		||||
    private readonly configService: ConfigService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  onApplicationBootstrap() {
 | 
			
		||||
    this._populate();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _populate = async (): Promise<void> => {
 | 
			
		||||
    const carpoolConfig: CarpoolConfig = this.configService.get<CarpoolConfig>(
 | 
			
		||||
      'carpool',
 | 
			
		||||
    ) as CarpoolConfig;
 | 
			
		||||
    const paginationConfig: PaginationConfig =
 | 
			
		||||
      this.configService.get<PaginationConfig>(
 | 
			
		||||
        'pagination',
 | 
			
		||||
      ) as PaginationConfig;
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      this._populateConfig(ConfigurationDomain.CARPOOL, carpoolConfig),
 | 
			
		||||
      this._populateConfig(ConfigurationDomain.PAGINATION, paginationConfig),
 | 
			
		||||
    ]);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _populateConfig = async <T>(
 | 
			
		||||
    domain: ConfigurationDomain,
 | 
			
		||||
    config: T,
 | 
			
		||||
  ): Promise<void> => {
 | 
			
		||||
    let key: keyof typeof config;
 | 
			
		||||
    for (key in config) {
 | 
			
		||||
      try {
 | 
			
		||||
        await this.configurationRepository.get({
 | 
			
		||||
          domain,
 | 
			
		||||
          key,
 | 
			
		||||
        });
 | 
			
		||||
      } catch (error: any) {
 | 
			
		||||
        if (error instanceof NotFoundException) {
 | 
			
		||||
          this.configurationRepository.set(
 | 
			
		||||
            {
 | 
			
		||||
              domain,
 | 
			
		||||
              key,
 | 
			
		||||
            },
 | 
			
		||||
            `${config[key]}`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,10 +3,7 @@ import { v4 } from 'uuid';
 | 
			
		|||
import {
 | 
			
		||||
  ConfigurationProps,
 | 
			
		||||
  CreateConfigurationProps,
 | 
			
		||||
  UpdateConfigurationProps,
 | 
			
		||||
} from './configuration.types';
 | 
			
		||||
import { ConfigurationSetDomainEvent } from './events/configuration-set.domain-event';
 | 
			
		||||
import { ConfigurationDeletedDomainEvent } from './events/configuration-deleted.domain-event';
 | 
			
		||||
 | 
			
		||||
export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
 | 
			
		||||
  protected readonly _id: AggregateID;
 | 
			
		||||
| 
						 | 
				
			
			@ -15,39 +12,9 @@ export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
 | 
			
		|||
    const id = v4();
 | 
			
		||||
    const props: ConfigurationProps = { ...create };
 | 
			
		||||
    const configuration = new ConfigurationEntity({ id, props });
 | 
			
		||||
    configuration.addEvent(
 | 
			
		||||
      new ConfigurationSetDomainEvent({
 | 
			
		||||
        aggregateId: id,
 | 
			
		||||
        domain: props.domain,
 | 
			
		||||
        key: props.key,
 | 
			
		||||
        value: props.value,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    return configuration;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  update(props: UpdateConfigurationProps): void {
 | 
			
		||||
    this.props.value = props.value;
 | 
			
		||||
    this.addEvent(
 | 
			
		||||
      new ConfigurationSetDomainEvent({
 | 
			
		||||
        aggregateId: this._id,
 | 
			
		||||
        domain: this.props.domain,
 | 
			
		||||
        key: this.props.key,
 | 
			
		||||
        value: props.value,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete(): void {
 | 
			
		||||
    this.addEvent(
 | 
			
		||||
      new ConfigurationDeletedDomainEvent({
 | 
			
		||||
        aggregateId: this.id,
 | 
			
		||||
        domain: this.props.domain,
 | 
			
		||||
        key: this.props.key,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,36 @@
 | 
			
		|||
// All properties that a Configuration has
 | 
			
		||||
export interface ConfigurationProps {
 | 
			
		||||
  domain: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
  identifier: ConfigurationIdentifier;
 | 
			
		||||
  value: ConfigurationValue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Properties that are needed for a Configuration creation
 | 
			
		||||
export interface CreateConfigurationProps {
 | 
			
		||||
  domain: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
  identifier: ConfigurationIdentifier;
 | 
			
		||||
  value: ConfigurationValue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UpdateConfigurationProps {
 | 
			
		||||
  value: string;
 | 
			
		||||
export enum ConfigurationDomain {
 | 
			
		||||
  CARPOOL = 'CARPOOL',
 | 
			
		||||
  PAGINATION = 'PAGINATION',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Domain {
 | 
			
		||||
  AD = 'AD',
 | 
			
		||||
  MATCHER = 'MATCHER',
 | 
			
		||||
  USER = 'USER',
 | 
			
		||||
export type ConfigurationIdentifier = {
 | 
			
		||||
  domain: ConfigurationDomain;
 | 
			
		||||
  key: ConfigurationKey;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ConfigurationKey = string;
 | 
			
		||||
export type ConfigurationValue = string;
 | 
			
		||||
 | 
			
		||||
export interface CarpoolConfig {
 | 
			
		||||
  departureTimeMargin: number;
 | 
			
		||||
  role: string;
 | 
			
		||||
  seatsProposed: number;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strictFrequency: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PaginationConfig {
 | 
			
		||||
  perPage: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class ConfigurationDeletedDomainEvent extends DomainEvent {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: DomainEventProps<ConfigurationDeletedDomainEvent>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.domain = props.domain;
 | 
			
		||||
    this.key = props.key;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class ConfigurationSetDomainEvent extends DomainEvent {
 | 
			
		||||
  readonly domain: string;
 | 
			
		||||
  readonly key: string;
 | 
			
		||||
  readonly value: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: DomainEventProps<ConfigurationSetDomainEvent>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.domain = props.domain;
 | 
			
		||||
    this.key = props.key;
 | 
			
		||||
    this.value = props.value;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,60 +1,47 @@
 | 
			
		|||
import { Inject, Injectable, Logger } from '@nestjs/common';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import {
 | 
			
		||||
  LoggerBase,
 | 
			
		||||
  MessagePublisherPort,
 | 
			
		||||
  PrismaRepositoryBase,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
import { ConfigurationEntity } from '../core/domain/configuration.entity';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
 | 
			
		||||
import { PrismaService } from './prisma.service';
 | 
			
		||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '../configuration.di-tokens';
 | 
			
		||||
import { InjectRedis } from '@songkeys/nestjs-redis';
 | 
			
		||||
import { Redis } from 'ioredis';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigurationIdentifier,
 | 
			
		||||
  ConfigurationValue,
 | 
			
		||||
} from '../core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationEntity } from '../core/domain/configuration.entity';
 | 
			
		||||
import { ConfigurationMapper } from '../configuration.mapper';
 | 
			
		||||
import { NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export type ConfigurationBaseModel = {
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  domain: string;
 | 
			
		||||
export type ConfigurationReadModel = {
 | 
			
		||||
  key: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
};
 | 
			
		||||
export type ConfigurationWriteModel = ConfigurationReadModel;
 | 
			
		||||
 | 
			
		||||
export type ConfigurationReadModel = ConfigurationBaseModel & {
 | 
			
		||||
  createdAt: Date;
 | 
			
		||||
  updatedAt: Date;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ConfigurationWriteModel = ConfigurationBaseModel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *  Repository is used for retrieving/saving domain entities
 | 
			
		||||
 * */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ConfigurationRepository
 | 
			
		||||
  extends PrismaRepositoryBase<
 | 
			
		||||
    ConfigurationEntity,
 | 
			
		||||
    ConfigurationReadModel,
 | 
			
		||||
    ConfigurationWriteModel
 | 
			
		||||
  >
 | 
			
		||||
  implements ConfigurationRepositoryPort
 | 
			
		||||
{
 | 
			
		||||
export class ConfigurationRepository implements ConfigurationRepositoryPort {
 | 
			
		||||
  constructor(
 | 
			
		||||
    prisma: PrismaService,
 | 
			
		||||
    mapper: ConfigurationMapper,
 | 
			
		||||
    eventEmitter: EventEmitter2,
 | 
			
		||||
    @Inject(CONFIGURATION_MESSAGE_PUBLISHER)
 | 
			
		||||
    protected readonly messagePublisher: MessagePublisherPort,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(
 | 
			
		||||
      prisma.configuration,
 | 
			
		||||
      prisma,
 | 
			
		||||
      mapper,
 | 
			
		||||
      eventEmitter,
 | 
			
		||||
      new LoggerBase({
 | 
			
		||||
        logger: new Logger(ConfigurationRepository.name),
 | 
			
		||||
        domain: SERVICE_NAME,
 | 
			
		||||
        messagePublisher,
 | 
			
		||||
      }),
 | 
			
		||||
    @InjectRedis() private readonly redis: Redis,
 | 
			
		||||
    private readonly mapper: ConfigurationMapper,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  get = async (
 | 
			
		||||
    identifier: ConfigurationIdentifier,
 | 
			
		||||
  ): Promise<ConfigurationEntity> => {
 | 
			
		||||
    const key: string = `${identifier.domain}:${identifier.key}`;
 | 
			
		||||
    const value: ConfigurationValue | null = await this.redis.get(key);
 | 
			
		||||
    if (!value)
 | 
			
		||||
      throw new NotFoundException(
 | 
			
		||||
        `Configuration item not found for key ${key}`,
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
    return this.mapper.toDomain({
 | 
			
		||||
      key,
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  set = async (
 | 
			
		||||
    identifier: ConfigurationIdentifier,
 | 
			
		||||
    value: ConfigurationValue,
 | 
			
		||||
  ): Promise<void> => {
 | 
			
		||||
    await this.redis.set(`${identifier.domain}:${identifier.key}`, value);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
import { Injectable, OnModuleInit } from '@nestjs/common';
 | 
			
		||||
import { PrismaClient } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PrismaService extends PrismaClient implements OnModuleInit {
 | 
			
		||||
  async onModuleInit() {
 | 
			
		||||
    await this.$connect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,23 +4,18 @@ package configuration;
 | 
			
		|||
 | 
			
		||||
service ConfigurationService {
 | 
			
		||||
  rpc Get(ConfigurationByDomainKey) returns (Configuration);
 | 
			
		||||
  rpc Set(Configuration) returns (ConfigurationId);
 | 
			
		||||
  rpc Delete(ConfigurationByDomainKey) returns (Empty);
 | 
			
		||||
  rpc Propagate(Empty) returns (Empty);
 | 
			
		||||
}
 | 
			
		||||
message ConfigurationId {
 | 
			
		||||
  string id = 1;
 | 
			
		||||
  rpc Set(Configuration) returns (Empty);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ConfigurationByDomainKey {
 | 
			
		||||
  string domain = 1;
 | 
			
		||||
  string key = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Configuration {
 | 
			
		||||
  string id = 1;
 | 
			
		||||
  string domain = 2;
 | 
			
		||||
  string key = 3;
 | 
			
		||||
  string value = 4;
 | 
			
		||||
  string domain = 1;
 | 
			
		||||
  string key = 2;
 | 
			
		||||
  string value = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Empty {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,45 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  DatabaseErrorException,
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
  RpcExceptionCode,
 | 
			
		||||
  RpcValidationPipe,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { DeleteConfigurationRequestDto } from './dtos/delete-configuration.request.dto';
 | 
			
		||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
import { DeleteConfigurationCommand } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.command';
 | 
			
		||||
 | 
			
		||||
@UsePipes(
 | 
			
		||||
  new RpcValidationPipe({
 | 
			
		||||
    whitelist: true,
 | 
			
		||||
    forbidUnknownValues: false,
 | 
			
		||||
  }),
 | 
			
		||||
)
 | 
			
		||||
@Controller()
 | 
			
		||||
export class DeleteConfigurationGrpcController {
 | 
			
		||||
  constructor(private readonly commandBus: CommandBus) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod(GRPC_SERVICE_NAME, 'Delete')
 | 
			
		||||
  async delete(data: DeleteConfigurationRequestDto): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.commandBus.execute(new DeleteConfigurationCommand(data));
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      if (error instanceof NotFoundException)
 | 
			
		||||
        throw new RpcException({
 | 
			
		||||
          code: RpcExceptionCode.NOT_FOUND,
 | 
			
		||||
          message: error.message,
 | 
			
		||||
        });
 | 
			
		||||
      if (error instanceof DatabaseErrorException)
 | 
			
		||||
        throw new RpcException({
 | 
			
		||||
          code: RpcExceptionCode.INTERNAL,
 | 
			
		||||
          message: error.message,
 | 
			
		||||
        });
 | 
			
		||||
      throw new RpcException({
 | 
			
		||||
        code: RpcExceptionCode.UNKNOWN,
 | 
			
		||||
        message: error.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class DeleteConfigurationRequestDto {
 | 
			
		||||
  @IsEnum(Domain)
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  domain: Domain;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  key: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class GetConfigurationRequestDto {
 | 
			
		||||
  @IsEnum(Domain)
 | 
			
		||||
  @IsEnum(ConfigurationDomain)
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  domain: Domain;
 | 
			
		||||
  domain: ConfigurationDomain;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class SetConfigurationRequestDto {
 | 
			
		||||
  @IsEnum(Domain)
 | 
			
		||||
  @IsEnum(ConfigurationDomain)
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  domain: Domain;
 | 
			
		||||
  domain: ConfigurationDomain;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
 | 
			
		||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
import { PropagateConfigurationsCommand } from '@modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.command';
 | 
			
		||||
import { v4 } from 'uuid';
 | 
			
		||||
 | 
			
		||||
@UsePipes(
 | 
			
		||||
  new RpcValidationPipe({
 | 
			
		||||
    whitelist: false,
 | 
			
		||||
    forbidUnknownValues: false,
 | 
			
		||||
  }),
 | 
			
		||||
)
 | 
			
		||||
@Controller()
 | 
			
		||||
export class PropagateConfigurationsGrpcController {
 | 
			
		||||
  constructor(private readonly commandBus: CommandBus) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod(GRPC_SERVICE_NAME, 'Propagate')
 | 
			
		||||
  async propagate(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.commandBus.execute(
 | 
			
		||||
        new PropagateConfigurationsCommand({ id: v4() }),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      throw new RpcException({
 | 
			
		||||
        code: RpcExceptionCode.UNKNOWN,
 | 
			
		||||
        message: error.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { IdResponse } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
 | 
			
		||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
| 
						 | 
				
			
			@ -22,12 +20,11 @@ export class SetConfigurationGrpcController {
 | 
			
		|||
  @GrpcMethod(GRPC_SERVICE_NAME, 'Set')
 | 
			
		||||
  async set(
 | 
			
		||||
    setConfigurationRequestDto: SetConfigurationRequestDto,
 | 
			
		||||
  ): Promise<IdResponse> {
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const aggregateID: AggregateID = await this.commandBus.execute(
 | 
			
		||||
      await this.commandBus.execute(
 | 
			
		||||
        new SetConfigurationCommand(setConfigurationRequestDto),
 | 
			
		||||
      );
 | 
			
		||||
      return new IdResponse(aggregateID);
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      throw new RpcException({
 | 
			
		||||
        code: RpcExceptionCode.UNKNOWN,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,174 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
  CONFIGURATION_REPOSITORY,
 | 
			
		||||
} from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import {
 | 
			
		||||
  CreateConfigurationProps,
 | 
			
		||||
  Domain,
 | 
			
		||||
} from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
 | 
			
		||||
import { PrismaService } from '@modules/configuration/infrastructure/prisma.service';
 | 
			
		||||
import { ConfigModule } from '@nestjs/config';
 | 
			
		||||
import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
describe('Configuration Repository', () => {
 | 
			
		||||
  let prismaService: PrismaService;
 | 
			
		||||
  let configurationRepository: ConfigurationRepository;
 | 
			
		||||
 | 
			
		||||
  const executeInsertCommand = async (table: string, object: any) => {
 | 
			
		||||
    const command = `INSERT INTO "${table}" ("${Object.keys(object).join(
 | 
			
		||||
      '","',
 | 
			
		||||
    )}") VALUES ('${Object.values(object).join("','")}')`;
 | 
			
		||||
    await prismaService.$executeRawUnsafe(command);
 | 
			
		||||
  };
 | 
			
		||||
  const getSeed = (index: number, uuid: string): string => {
 | 
			
		||||
    return `${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const baseUuid = {
 | 
			
		||||
    uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createConfigurations = async (nbToCreate = 10) => {
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      const configurationToCreate = {
 | 
			
		||||
        uuid: getSeed(i, baseUuid.uuid),
 | 
			
		||||
        domain: Domain.AD,
 | 
			
		||||
        key: `key${i}`,
 | 
			
		||||
        value: `value${i}`,
 | 
			
		||||
        createdAt: '2023-07-24 13:07:05.000',
 | 
			
		||||
        updatedAt: '2023-07-24 13:07:05.000',
 | 
			
		||||
      };
 | 
			
		||||
      configurationToCreate.uuid = getSeed(i, baseUuid.uuid);
 | 
			
		||||
      await executeInsertCommand('configuration', configurationToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockMessagePublisher = {
 | 
			
		||||
    publish: jest.fn().mockImplementation(),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockLogger = {
 | 
			
		||||
    log: jest.fn(),
 | 
			
		||||
    warn: jest.fn(),
 | 
			
		||||
    error: jest.fn(),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module = await Test.createTestingModule({
 | 
			
		||||
      imports: [
 | 
			
		||||
        EventEmitterModule.forRoot(),
 | 
			
		||||
        ConfigModule.forRoot({ isGlobal: true }),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PrismaService,
 | 
			
		||||
        ConfigurationMapper,
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_REPOSITORY,
 | 
			
		||||
          useClass: ConfigurationRepository,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
          useValue: mockMessagePublisher,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
      // disable logging
 | 
			
		||||
      .setLogger(mockLogger)
 | 
			
		||||
      .compile();
 | 
			
		||||
 | 
			
		||||
    prismaService = module.get<PrismaService>(PrismaService);
 | 
			
		||||
    configurationRepository = module.get<ConfigurationRepository>(
 | 
			
		||||
      CONFIGURATION_REPOSITORY,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterAll(async () => {
 | 
			
		||||
    await prismaService.$disconnect();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await prismaService.configuration.deleteMany();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findOne', () => {
 | 
			
		||||
    it('should return a configuration', async () => {
 | 
			
		||||
      await createConfigurations(1);
 | 
			
		||||
      const result = await configurationRepository.findOne({
 | 
			
		||||
        domain: Domain.AD,
 | 
			
		||||
        key: 'key0',
 | 
			
		||||
      });
 | 
			
		||||
      expect(result.getProps().value).toBe('value0');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findAll', () => {
 | 
			
		||||
    it('should return all configurations', async () => {
 | 
			
		||||
      await createConfigurations(10);
 | 
			
		||||
      const configurations: ConfigurationEntity[] =
 | 
			
		||||
        await configurationRepository.findAll({});
 | 
			
		||||
      expect(configurations).toHaveLength(10);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('create', () => {
 | 
			
		||||
    it('should create a configuration', async () => {
 | 
			
		||||
      const beforeCount = await prismaService.configuration.count();
 | 
			
		||||
 | 
			
		||||
      const createConfigurationProps: CreateConfigurationProps = {
 | 
			
		||||
        domain: Domain.AD,
 | 
			
		||||
        key: 'seatsProposed',
 | 
			
		||||
        value: '3',
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const configurationToCreate: ConfigurationEntity =
 | 
			
		||||
        ConfigurationEntity.create(createConfigurationProps);
 | 
			
		||||
      await configurationRepository.insert(configurationToCreate);
 | 
			
		||||
 | 
			
		||||
      const afterCount = await prismaService.configuration.count();
 | 
			
		||||
 | 
			
		||||
      expect(afterCount - beforeCount).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('update', () => {
 | 
			
		||||
    it('should update a configuration', async () => {
 | 
			
		||||
      await createConfigurations(1);
 | 
			
		||||
      const configurationToUpdate: ConfigurationEntity =
 | 
			
		||||
        await configurationRepository.findOne({
 | 
			
		||||
          domain: Domain.AD,
 | 
			
		||||
          key: 'key0',
 | 
			
		||||
        });
 | 
			
		||||
      configurationToUpdate.update({ value: 'newValue' });
 | 
			
		||||
      await configurationRepository.update(
 | 
			
		||||
        configurationToUpdate.id,
 | 
			
		||||
        configurationToUpdate,
 | 
			
		||||
      );
 | 
			
		||||
      const result: ConfigurationEntity = await configurationRepository.findOne(
 | 
			
		||||
        {
 | 
			
		||||
          domain: Domain.AD,
 | 
			
		||||
          key: 'key0',
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(result.getProps().value).toBe('newValue');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('delete', () => {
 | 
			
		||||
    it('should delete a configuration', async () => {
 | 
			
		||||
      await createConfigurations(10);
 | 
			
		||||
      const beforeCount = await prismaService.configuration.count();
 | 
			
		||||
      const configurationToDelete: ConfigurationEntity =
 | 
			
		||||
        await configurationRepository.findOne({
 | 
			
		||||
          domain: Domain.AD,
 | 
			
		||||
          key: 'key4',
 | 
			
		||||
        });
 | 
			
		||||
      await configurationRepository.delete(configurationToDelete);
 | 
			
		||||
      const afterCount = await prismaService.configuration.count();
 | 
			
		||||
      expect(afterCount - beforeCount).toBe(-1);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigurationReadModel,
 | 
			
		||||
  ConfigurationWriteModel,
 | 
			
		||||
| 
						 | 
				
			
			@ -12,20 +12,18 @@ const now = new Date('2023-06-21 06:00:00');
 | 
			
		|||
const configurationEntity: ConfigurationEntity = new ConfigurationEntity({
 | 
			
		||||
  id: 'c160cf8c-f057-4962-841f-3ad68346df44',
 | 
			
		||||
  props: {
 | 
			
		||||
    domain: Domain.AD,
 | 
			
		||||
    identifier: {
 | 
			
		||||
      domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
    },
 | 
			
		||||
    value: '3',
 | 
			
		||||
  },
 | 
			
		||||
  createdAt: now,
 | 
			
		||||
  updatedAt: now,
 | 
			
		||||
});
 | 
			
		||||
const configurationReadModel: ConfigurationReadModel = {
 | 
			
		||||
  uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
 | 
			
		||||
  domain: 'AD',
 | 
			
		||||
  key: 'seatsProposed',
 | 
			
		||||
  key: 'AD:seatsProposed',
 | 
			
		||||
  value: '4',
 | 
			
		||||
  createdAt: now,
 | 
			
		||||
  updatedAt: now,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Configuration Mapper', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,17 @@
 | 
			
		|||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import {
 | 
			
		||||
  CreateConfigurationProps,
 | 
			
		||||
  Domain,
 | 
			
		||||
  UpdateConfigurationProps,
 | 
			
		||||
  ConfigurationDomain,
 | 
			
		||||
} from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDeletedDomainEvent } from '@modules/configuration/core/domain/events/configuration-deleted.domain-event';
 | 
			
		||||
import { ConfigurationSetDomainEvent } from '@modules/configuration/core/domain/events/configuration-set.domain-event';
 | 
			
		||||
 | 
			
		||||
const createConfigurationProps: CreateConfigurationProps = {
 | 
			
		||||
  domain: Domain.AD,
 | 
			
		||||
  identifier: {
 | 
			
		||||
    domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
    key: 'seatsProposed',
 | 
			
		||||
  },
 | 
			
		||||
  value: '3',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateConfigurationProps: UpdateConfigurationProps = {
 | 
			
		||||
  value: '2',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Configuration entity create', () => {
 | 
			
		||||
  it('should create a new configuration entity', async () => {
 | 
			
		||||
    const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
 | 
			
		||||
| 
						 | 
				
			
			@ -24,38 +19,5 @@ describe('Configuration entity create', () => {
 | 
			
		|||
    );
 | 
			
		||||
    expect(configurationEntity.id.length).toBe(36);
 | 
			
		||||
    expect(configurationEntity.getProps().value).toBe('3');
 | 
			
		||||
    expect(configurationEntity.domainEvents.length).toBe(1);
 | 
			
		||||
    expect(configurationEntity.domainEvents[0]).toBeInstanceOf(
 | 
			
		||||
      ConfigurationSetDomainEvent,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Configuration entity update', () => {
 | 
			
		||||
  it('should update a configuration entity', async () => {
 | 
			
		||||
    const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
 | 
			
		||||
      createConfigurationProps,
 | 
			
		||||
    );
 | 
			
		||||
    configurationEntity.update(updateConfigurationProps);
 | 
			
		||||
    expect(configurationEntity.getProps().value).toBe('2');
 | 
			
		||||
    // 2 events because ConfigurationEntity.create sends a ConfigurationSetDomainEvent
 | 
			
		||||
    expect(configurationEntity.domainEvents.length).toBe(2);
 | 
			
		||||
    expect(configurationEntity.domainEvents[1]).toBeInstanceOf(
 | 
			
		||||
      ConfigurationSetDomainEvent,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Configuration entity delete', () => {
 | 
			
		||||
  it('should delete a configuration entity', async () => {
 | 
			
		||||
    const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
 | 
			
		||||
      createConfigurationProps,
 | 
			
		||||
    );
 | 
			
		||||
    configurationEntity.delete();
 | 
			
		||||
    // 2 events because ConfigurationEntity.create sends a ConfigurationSetDomainEvent
 | 
			
		||||
    expect(configurationEntity.domainEvents.length).toBe(2);
 | 
			
		||||
    expect(configurationEntity.domainEvents[1]).toBeInstanceOf(
 | 
			
		||||
      ConfigurationDeletedDomainEvent,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { DeleteConfigurationCommand } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.command';
 | 
			
		||||
import { DeleteConfigurationService } from '@modules/configuration/core/application/commands/delete-configuration/delete-configuration.service';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { DeleteConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const deleteConfigurationRequest: DeleteConfigurationRequestDto = {
 | 
			
		||||
  domain: Domain.AD,
 | 
			
		||||
  key: 'seatsProposed',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockConfigurationEntity = {
 | 
			
		||||
  delete: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockConfigurationRepository = {
 | 
			
		||||
  findOne: jest.fn().mockImplementation(() => mockConfigurationEntity),
 | 
			
		||||
  delete: jest.fn().mockImplementationOnce(() => true),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Delete Configuration Service', () => {
 | 
			
		||||
  let deleteConfigurationService: DeleteConfigurationService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_REPOSITORY,
 | 
			
		||||
          useValue: mockConfigurationRepository,
 | 
			
		||||
        },
 | 
			
		||||
        DeleteConfigurationService,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    deleteConfigurationService = module.get<DeleteConfigurationService>(
 | 
			
		||||
      DeleteConfigurationService,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(deleteConfigurationService).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execution', () => {
 | 
			
		||||
    const deleteConfigurationCommand = new DeleteConfigurationCommand(
 | 
			
		||||
      deleteConfigurationRequest,
 | 
			
		||||
    );
 | 
			
		||||
    it('should delete a configuration item', async () => {
 | 
			
		||||
      const result: boolean = await deleteConfigurationService.execute(
 | 
			
		||||
        deleteConfigurationCommand,
 | 
			
		||||
      );
 | 
			
		||||
      expect(result).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
 | 
			
		||||
| 
						 | 
				
			
			@ -9,8 +9,10 @@ const now = new Date('2023-06-21 06:00:00');
 | 
			
		|||
const configuration: ConfigurationEntity = new ConfigurationEntity({
 | 
			
		||||
  id: 'c160cf8c-f057-4962-841f-3ad68346df44',
 | 
			
		||||
  props: {
 | 
			
		||||
    domain: Domain.AD,
 | 
			
		||||
    identifier: {
 | 
			
		||||
      domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
    },
 | 
			
		||||
    value: '3',
 | 
			
		||||
  },
 | 
			
		||||
  createdAt: now,
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +20,7 @@ const configuration: ConfigurationEntity = new ConfigurationEntity({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
const mockConfigurationRepository = {
 | 
			
		||||
  findOne: jest.fn().mockImplementation(() => configuration),
 | 
			
		||||
  get: jest.fn().mockImplementation(() => configuration),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Get Configuration Query Handler', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +49,7 @@ describe('Get Configuration Query Handler', () => {
 | 
			
		|||
  describe('execution', () => {
 | 
			
		||||
    it('should return a configuration item', async () => {
 | 
			
		||||
      const getConfigurationQuery = new GetConfigurationQuery(
 | 
			
		||||
        Domain.AD,
 | 
			
		||||
        ConfigurationDomain.CARPOOL,
 | 
			
		||||
        'seatsProposed',
 | 
			
		||||
      );
 | 
			
		||||
      const configuration: ConfigurationEntity =
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
import { NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { PopulateService } from '@modules/configuration/core/application/services/populate.service';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const mockConfigurationRepository = {
 | 
			
		||||
  get: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(
 | 
			
		||||
      () =>
 | 
			
		||||
        new ConfigurationEntity({
 | 
			
		||||
          id: '001199d4-7187-4e83-a044-12159cba2e33',
 | 
			
		||||
          props: {
 | 
			
		||||
            identifier: {
 | 
			
		||||
              domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
              key: 'someKey',
 | 
			
		||||
            },
 | 
			
		||||
            value: 'someValue',
 | 
			
		||||
          },
 | 
			
		||||
          createdAt: new Date('2023-10-23'),
 | 
			
		||||
          updatedAt: new Date('2023-10-23'),
 | 
			
		||||
        }),
 | 
			
		||||
    )
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new NotFoundException('Configuration not found');
 | 
			
		||||
    }),
 | 
			
		||||
  set: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockConfigService = {
 | 
			
		||||
  get: jest.fn().mockImplementation((domain: string) => {
 | 
			
		||||
    switch (domain) {
 | 
			
		||||
      case 'carpool':
 | 
			
		||||
        return {
 | 
			
		||||
          departureTimeMargin: 900,
 | 
			
		||||
          role: 'passenger',
 | 
			
		||||
          seatsProposed: 3,
 | 
			
		||||
          seatsRequested: 1,
 | 
			
		||||
          strictFrequency: false,
 | 
			
		||||
        };
 | 
			
		||||
      case 'pagination':
 | 
			
		||||
        return {
 | 
			
		||||
          perPage: 10,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Populate Service', () => {
 | 
			
		||||
  let populateService: PopulateService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_REPOSITORY,
 | 
			
		||||
          useValue: mockConfigurationRepository,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: ConfigService,
 | 
			
		||||
          useValue: mockConfigService,
 | 
			
		||||
        },
 | 
			
		||||
        PopulateService,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    populateService = module.get<PopulateService>(PopulateService);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(populateService).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should populate database with default values', () => {
 | 
			
		||||
    jest.spyOn(mockConfigurationRepository, 'get');
 | 
			
		||||
    jest.spyOn(mockConfigurationRepository, 'set');
 | 
			
		||||
    populateService.onApplicationBootstrap();
 | 
			
		||||
    expect(mockConfigurationRepository.get).toHaveBeenCalled();
 | 
			
		||||
    expect(mockConfigurationRepository.set).toHaveBeenCalled();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,100 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import {
 | 
			
		||||
  CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
  CONFIGURATION_REPOSITORY,
 | 
			
		||||
} from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { PropagateConfigurationsService } from '@modules/configuration/core/application/commands/propagate-configurations/propagate-configurations.service';
 | 
			
		||||
import { CONFIGURATION_PROPAGATED_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const configurationEntities = [
 | 
			
		||||
  new ConfigurationEntity({
 | 
			
		||||
    id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
    props: {
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
      value: '3',
 | 
			
		||||
    },
 | 
			
		||||
    createdAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
    updatedAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
  }),
 | 
			
		||||
  new ConfigurationEntity({
 | 
			
		||||
    id: '047a6ecf-23d4-4d3e-877c-3225d560a8db',
 | 
			
		||||
    props: {
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsRequested',
 | 
			
		||||
      value: '1',
 | 
			
		||||
    },
 | 
			
		||||
    createdAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
    updatedAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const mockConfigurationMapper = {
 | 
			
		||||
  toResponse: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
      value: '3',
 | 
			
		||||
    }))
 | 
			
		||||
    .mockImplementationOnce(() => ({
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsRequested',
 | 
			
		||||
      value: '1',
 | 
			
		||||
    })),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockConfigurationRepository = {
 | 
			
		||||
  findAll: jest.fn().mockImplementationOnce(() => configurationEntities),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Propagate Configurations Service', () => {
 | 
			
		||||
  let propagateConfigurationsService: PropagateConfigurationsService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_REPOSITORY,
 | 
			
		||||
          useValue: mockConfigurationRepository,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
          useValue: mockMessagePublisher,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: ConfigurationMapper,
 | 
			
		||||
          useValue: mockConfigurationMapper,
 | 
			
		||||
        },
 | 
			
		||||
        PropagateConfigurationsService,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    propagateConfigurationsService = module.get<PropagateConfigurationsService>(
 | 
			
		||||
      PropagateConfigurationsService,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(propagateConfigurationsService).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execution', () => {
 | 
			
		||||
    it('should propagate configuration items', async () => {
 | 
			
		||||
      jest.spyOn(mockMessagePublisher, 'publish');
 | 
			
		||||
      await propagateConfigurationsService.execute();
 | 
			
		||||
      expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
 | 
			
		||||
        CONFIGURATION_PROPAGATED_ROUTING_KEY,
 | 
			
		||||
        '[{"domain":"AD","key":"seatsProposed","value":"3"},{"domain":"AD","key":"seatsRequested","value":"1"}]',
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { PublishMessageWhenConfigurationIsDeletedDomainEventHandler } from '@modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-deleted.domain-event-handler';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDeletedDomainEvent } from '@modules/configuration/core/domain/events/configuration-deleted.domain-event';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { CONFIGURATION_DELETED_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Publish message when configuration is deleted domain event handler', () => {
 | 
			
		||||
  let publishMessageWhenConfigurationIsDeletedDomainEventHandler: PublishMessageWhenConfigurationIsDeletedDomainEventHandler;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
          useValue: mockMessagePublisher,
 | 
			
		||||
        },
 | 
			
		||||
        PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    publishMessageWhenConfigurationIsDeletedDomainEventHandler =
 | 
			
		||||
      module.get<PublishMessageWhenConfigurationIsDeletedDomainEventHandler>(
 | 
			
		||||
        PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should publish a message', () => {
 | 
			
		||||
    jest.spyOn(mockMessagePublisher, 'publish');
 | 
			
		||||
    const configurationDeletedDomainEvent: ConfigurationDeletedDomainEvent = {
 | 
			
		||||
      id: 'some-domain-event-id',
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
      aggregateId: 'some-aggregate-id',
 | 
			
		||||
      metadata: {
 | 
			
		||||
        timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
 | 
			
		||||
        correlationId: 'some-correlation-id',
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    publishMessageWhenConfigurationIsDeletedDomainEventHandler.handle(
 | 
			
		||||
      configurationDeletedDomainEvent,
 | 
			
		||||
    );
 | 
			
		||||
    expect(
 | 
			
		||||
      publishMessageWhenConfigurationIsDeletedDomainEventHandler,
 | 
			
		||||
    ).toBeDefined();
 | 
			
		||||
    expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
    expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
 | 
			
		||||
      CONFIGURATION_DELETED_ROUTING_KEY,
 | 
			
		||||
      '{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"domain":"AD","key":"seatsProposed"}',
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { PublishMessageWhenConfigurationIsSetDomainEventHandler } from '@modules/configuration/core/application/event-handlers/publish-message-when-configuration-is-set.domain-event-handler';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationSetDomainEvent } from '@modules/configuration/core/domain/events/configuration-set.domain-event';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { CONFIGURATION_SET_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Publish message when configuration is set domain event handler', () => {
 | 
			
		||||
  let publishMessageWhenConfigurationIsSetDomainEventHandler: PublishMessageWhenConfigurationIsSetDomainEventHandler;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CONFIGURATION_MESSAGE_PUBLISHER,
 | 
			
		||||
          useValue: mockMessagePublisher,
 | 
			
		||||
        },
 | 
			
		||||
        PublishMessageWhenConfigurationIsSetDomainEventHandler,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    publishMessageWhenConfigurationIsSetDomainEventHandler =
 | 
			
		||||
      module.get<PublishMessageWhenConfigurationIsSetDomainEventHandler>(
 | 
			
		||||
        PublishMessageWhenConfigurationIsSetDomainEventHandler,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should publish a message', () => {
 | 
			
		||||
    jest.spyOn(mockMessagePublisher, 'publish');
 | 
			
		||||
    const configurationSetDomainEvent: ConfigurationSetDomainEvent = {
 | 
			
		||||
      id: 'some-domain-event-id',
 | 
			
		||||
      aggregateId: 'some-aggregate-id',
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
      value: '3',
 | 
			
		||||
      metadata: {
 | 
			
		||||
        timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
 | 
			
		||||
        correlationId: 'some-correlation-id',
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    publishMessageWhenConfigurationIsSetDomainEventHandler.handle(
 | 
			
		||||
      configurationSetDomainEvent,
 | 
			
		||||
    );
 | 
			
		||||
    expect(
 | 
			
		||||
      publishMessageWhenConfigurationIsSetDomainEventHandler,
 | 
			
		||||
    ).toBeDefined();
 | 
			
		||||
    expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
    expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
 | 
			
		||||
      CONFIGURATION_SET_ROUTING_KEY,
 | 
			
		||||
      '{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"domain":"AD","key":"seatsProposed","value":"3"}',
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,47 +1,19 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service';
 | 
			
		||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
 | 
			
		||||
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
 | 
			
		||||
const setConfigurationRequest: SetConfigurationRequestDto = {
 | 
			
		||||
  domain: Domain.AD,
 | 
			
		||||
  domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
  key: 'seatsProposed',
 | 
			
		||||
  value: '3',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const existingConfigurationEntity = new ConfigurationEntity({
 | 
			
		||||
  id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
  props: {
 | 
			
		||||
    domain: Domain.AD,
 | 
			
		||||
    key: 'seatsProposed',
 | 
			
		||||
    value: '2',
 | 
			
		||||
  },
 | 
			
		||||
  createdAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
  updatedAt: new Date('2023-10-23T07:00:00Z'),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mockConfigurationRepository = {
 | 
			
		||||
  findOne: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new NotFoundException();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new NotFoundException();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => existingConfigurationEntity)
 | 
			
		||||
    .mockImplementationOnce(() => existingConfigurationEntity),
 | 
			
		||||
  insert: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({}))
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Error();
 | 
			
		||||
    }),
 | 
			
		||||
  update: jest
 | 
			
		||||
  set: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({}))
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -76,31 +48,13 @@ describe('Set Configuration Service', () => {
 | 
			
		|||
    const setConfigurationCommand = new SetConfigurationCommand(
 | 
			
		||||
      setConfigurationRequest,
 | 
			
		||||
    );
 | 
			
		||||
    it('should create a new configuration item', async () => {
 | 
			
		||||
    it('should set an existing configuration item', async () => {
 | 
			
		||||
      jest.spyOn(mockConfigurationRepository, 'set');
 | 
			
		||||
      ConfigurationEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      const result: AggregateID = await setConfigurationService.execute(
 | 
			
		||||
        setConfigurationCommand,
 | 
			
		||||
      );
 | 
			
		||||
      expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an error if something bad happens on configuration item creation', async () => {
 | 
			
		||||
      ConfigurationEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      await expect(
 | 
			
		||||
        setConfigurationService.execute(setConfigurationCommand),
 | 
			
		||||
      ).rejects.toBeInstanceOf(Error);
 | 
			
		||||
    });
 | 
			
		||||
    it('should update an existing configuration item', async () => {
 | 
			
		||||
      ConfigurationEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      const result: AggregateID = await setConfigurationService.execute(
 | 
			
		||||
        setConfigurationCommand,
 | 
			
		||||
      );
 | 
			
		||||
      expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
 | 
			
		||||
      await setConfigurationService.execute(setConfigurationCommand);
 | 
			
		||||
      expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an error if something bad happens on configuration item update', async () => {
 | 
			
		||||
      ConfigurationEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +1,93 @@
 | 
			
		|||
import { NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
 | 
			
		||||
import { PrismaService } from '@modules/configuration/infrastructure/prisma.service';
 | 
			
		||||
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { getRedisToken } from '@songkeys/nestjs-redis';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
const mockRedis = {
 | 
			
		||||
  get: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => '1')
 | 
			
		||||
    .mockImplementation(() => null),
 | 
			
		||||
  set: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Configuration repository', () => {
 | 
			
		||||
  let prismaService: PrismaService;
 | 
			
		||||
  let configurationMapper: ConfigurationMapper;
 | 
			
		||||
  let eventEmitter: EventEmitter2;
 | 
			
		||||
const mockConfigurationMapper = {
 | 
			
		||||
  toDomain: jest.fn().mockImplementation(
 | 
			
		||||
    () =>
 | 
			
		||||
      new ConfigurationEntity({
 | 
			
		||||
        id: '001199d4-7187-4e83-a044-12159cba2e33',
 | 
			
		||||
        props: {
 | 
			
		||||
          identifier: {
 | 
			
		||||
            domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
            key: 'seatsProposed',
 | 
			
		||||
          },
 | 
			
		||||
          value: '1',
 | 
			
		||||
        },
 | 
			
		||||
        createdAt: new Date('2023-10-23'),
 | 
			
		||||
        updatedAt: new Date('2023-10-23'),
 | 
			
		||||
      }),
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Configuration Repository', () => {
 | 
			
		||||
  let configurationRepository: ConfigurationRepository;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [EventEmitterModule.forRoot()],
 | 
			
		||||
      providers: [PrismaService, ConfigurationMapper],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: getRedisToken('default'),
 | 
			
		||||
          useValue: mockRedis,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: ConfigurationMapper,
 | 
			
		||||
          useValue: mockConfigurationMapper,
 | 
			
		||||
        },
 | 
			
		||||
        ConfigurationRepository,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    prismaService = module.get<PrismaService>(PrismaService);
 | 
			
		||||
    configurationMapper = module.get<ConfigurationMapper>(ConfigurationMapper);
 | 
			
		||||
    eventEmitter = module.get<EventEmitter2>(EventEmitter2);
 | 
			
		||||
    configurationRepository = module.get<ConfigurationRepository>(
 | 
			
		||||
      ConfigurationRepository,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(configurationRepository).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('interact', () => {
 | 
			
		||||
    it('should get a value', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
      new ConfigurationRepository(
 | 
			
		||||
        prismaService,
 | 
			
		||||
        configurationMapper,
 | 
			
		||||
        eventEmitter,
 | 
			
		||||
        mockMessagePublisher,
 | 
			
		||||
        (
 | 
			
		||||
          await configurationRepository.get({
 | 
			
		||||
            domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
            key: 'seatsProposed',
 | 
			
		||||
          })
 | 
			
		||||
        ).getProps().value,
 | 
			
		||||
      ).toBe('1');
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw if configuration is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        configurationRepository.get({
 | 
			
		||||
          domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
          key: 'seatsProposed',
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(NotFoundException);
 | 
			
		||||
    });
 | 
			
		||||
    it('should set a value', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await configurationRepository.set(
 | 
			
		||||
          {
 | 
			
		||||
            domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
            key: 'seatsProposed',
 | 
			
		||||
          },
 | 
			
		||||
          '3',
 | 
			
		||||
        ),
 | 
			
		||||
    ).toBeDefined();
 | 
			
		||||
      ).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,108 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  DatabaseErrorException,
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { DeleteConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/delete-configuration.grpc.controller';
 | 
			
		||||
import { DeleteConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/delete-configuration.request.dto';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const deleteConfigurationRequest: DeleteConfigurationRequestDto = {
 | 
			
		||||
  domain: Domain.AD,
 | 
			
		||||
  key: 'seatsProposed',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockCommandBus = {
 | 
			
		||||
  execute: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({}))
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new NotFoundException();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new DatabaseErrorException();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Error();
 | 
			
		||||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Delete Configuration Grpc Controller', () => {
 | 
			
		||||
  let deleteConfigurationGrpcController: DeleteConfigurationGrpcController;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CommandBus,
 | 
			
		||||
          useValue: mockCommandBus,
 | 
			
		||||
        },
 | 
			
		||||
        DeleteConfigurationGrpcController,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    deleteConfigurationGrpcController =
 | 
			
		||||
      module.get<DeleteConfigurationGrpcController>(
 | 
			
		||||
        DeleteConfigurationGrpcController,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    jest.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(deleteConfigurationGrpcController).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should delete a configuration item', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    await deleteConfigurationGrpcController.delete(deleteConfigurationRequest);
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should throw a dedicated RpcException if configuration item does not exist', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await deleteConfigurationGrpcController.delete(
 | 
			
		||||
        deleteConfigurationRequest,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should throw a dedicated RpcException if a database error occurs', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await deleteConfigurationGrpcController.delete(
 | 
			
		||||
        deleteConfigurationRequest,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.INTERNAL);
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should throw a generic RpcException', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await deleteConfigurationGrpcController.delete(
 | 
			
		||||
        deleteConfigurationRequest,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { NotFoundException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller';
 | 
			
		||||
import { QueryBus } from '@nestjs/cqrs';
 | 
			
		||||
import { RpcException } from '@nestjs/microservices';
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ const mockQueryBus = {
 | 
			
		|||
 | 
			
		||||
const mockConfigurationMapper = {
 | 
			
		||||
  toResponse: jest.fn().mockImplementationOnce(() => ({
 | 
			
		||||
    domain: Domain.AD,
 | 
			
		||||
    domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
    key: 'seatsProposed',
 | 
			
		||||
    value: '3',
 | 
			
		||||
  })),
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ describe('Get Configuration Grpc Controller', () => {
 | 
			
		|||
    jest.spyOn(mockQueryBus, 'execute');
 | 
			
		||||
    jest.spyOn(mockConfigurationMapper, 'toResponse');
 | 
			
		||||
    const response = await getConfigurationGrpcController.get({
 | 
			
		||||
      domain: Domain.AD,
 | 
			
		||||
      domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
      key: 'seatsProposed',
 | 
			
		||||
    });
 | 
			
		||||
    expect(response.value).toBe('3');
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ describe('Get Configuration Grpc Controller', () => {
 | 
			
		|||
    expect.assertions(4);
 | 
			
		||||
    try {
 | 
			
		||||
      await getConfigurationGrpcController.get({
 | 
			
		||||
        domain: Domain.AD,
 | 
			
		||||
        domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
        key: 'price',
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +93,7 @@ describe('Get Configuration Grpc Controller', () => {
 | 
			
		|||
    expect.assertions(4);
 | 
			
		||||
    try {
 | 
			
		||||
      await getConfigurationGrpcController.get({
 | 
			
		||||
        domain: Domain.AD,
 | 
			
		||||
        domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
        key: 'someValue',
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,60 +0,0 @@
 | 
			
		|||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { PropagateConfigurationsGrpcController } from '@modules/configuration/interface/grpc-controllers/propagate-configurations.grpc.controller';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const mockCommandBus = {
 | 
			
		||||
  execute: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {})
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Error();
 | 
			
		||||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Propagate Configurations Grpc Controller', () => {
 | 
			
		||||
  let propagateConfigurationsGrpcController: PropagateConfigurationsGrpcController;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CommandBus,
 | 
			
		||||
          useValue: mockCommandBus,
 | 
			
		||||
        },
 | 
			
		||||
        PropagateConfigurationsGrpcController,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    propagateConfigurationsGrpcController =
 | 
			
		||||
      module.get<PropagateConfigurationsGrpcController>(
 | 
			
		||||
        PropagateConfigurationsGrpcController,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    jest.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(propagateConfigurationsGrpcController).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should propagate configuration items', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    await propagateConfigurationsGrpcController.propagate();
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should throw a generic RpcException', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    try {
 | 
			
		||||
      await propagateConfigurationsGrpcController.propagate();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import { IdResponse } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Domain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
 | 
			
		||||
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
 | 
			
		||||
import { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +7,7 @@ import { RpcException } from '@nestjs/microservices';
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const setConfigurationRequest: SetConfigurationRequestDto = {
 | 
			
		||||
  domain: Domain.AD,
 | 
			
		||||
  domain: ConfigurationDomain.CARPOOL,
 | 
			
		||||
  key: 'seatsProposed',
 | 
			
		||||
  value: '3',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -51,11 +50,7 @@ describe('Set Configuration Grpc Controller', () => {
 | 
			
		|||
 | 
			
		||||
  it('should set a configuration item', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    const result: IdResponse = await setConfigurationGrpcController.set(
 | 
			
		||||
      setConfigurationRequest,
 | 
			
		||||
    );
 | 
			
		||||
    expect(result).toBeInstanceOf(IdResponse);
 | 
			
		||||
    expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
 | 
			
		||||
    await setConfigurationGrpcController.set(setConfigurationRequest);
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,10 @@ const imports = [
 | 
			
		|||
    useFactory: async (
 | 
			
		||||
      configService: ConfigService,
 | 
			
		||||
    ): Promise<MessageBrokerModuleOptions> => ({
 | 
			
		||||
      uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
 | 
			
		||||
      uri: configService.get<string>('broker.uri') as string,
 | 
			
		||||
      exchange: {
 | 
			
		||||
        name: configService.get<string>('MESSAGE_BROKER_EXCHANGE') as string,
 | 
			
		||||
        durable: configService.get<boolean>(
 | 
			
		||||
          'MESSAGE_BROKER_EXCHANGE_DURABILITY',
 | 
			
		||||
        ) as boolean,
 | 
			
		||||
        name: configService.get<string>('broker.exchange') as string,
 | 
			
		||||
        durable: configService.get<boolean>('broker.durability') as boolean,
 | 
			
		||||
      },
 | 
			
		||||
      name: SERVICE_NAME,
 | 
			
		||||
    }),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue