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
|
SERVICE_PORT=5003
|
||||||
HEALTH_SERVICE_PORT=6003
|
HEALTH_SERVICE_PORT=6003
|
||||||
|
|
||||||
# PRISMA
|
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=configuration"
|
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
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_URL=0.0.0.0
|
||||||
SERVICE_PORT=5003
|
SERVICE_PORT=5003
|
||||||
|
|
||||||
# PRISMA
|
# REDIS
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=configuration"
|
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
|
- docker-compose -f docker-compose.ci.tools.yml -p configuration-tools --env-file ci/.env.ci up -d
|
||||||
- sh ci/wait-up.sh
|
- sh ci/wait-up.sh
|
||||||
- docker-compose -f docker-compose.ci.service.yml -p configuration-service --env-file ci/.env.ci up -d
|
- 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\.]+)/
|
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
- 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.
|
# Copying this first prevents re-running npm install on every code change.
|
||||||
COPY --chown=node:node package*.json ./
|
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`
|
# Install app dependencies using the `npm ci` command instead of `npm install`
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
RUN npx prisma generate
|
|
||||||
|
|
||||||
# Bundle app source
|
# Bundle app source
|
||||||
COPY --chown=node:node . .
|
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 --chown=node:node . .
|
||||||
|
|
||||||
# Copy prisma (needed for migrations)
|
|
||||||
COPY --chown=node:node ./prisma prisma
|
|
||||||
|
|
||||||
# Run the build command which creates the production bundle
|
# Run the build command which creates the production bundle
|
||||||
RUN npm run build
|
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 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/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
|
COPY --chown=node:node --from=build /usr/src/app/dist ./dist
|
||||||
|
|
||||||
# Start the server using the production build
|
# Start the server using the production build
|
||||||
|
|
58
README.md
58
README.md
|
@ -1,12 +1,9 @@
|
||||||
# Mobicoop V3 - Configuration Service
|
# Mobicoop V3 - Configuration Service
|
||||||
|
|
||||||
Configuration items management. Used to configure all services using a broker to disseminate the configuration items.
|
Configuration items management. Used to configure and store all services using redis as database.
|
||||||
|
|
||||||
This service handles the persistence of the configuration items of all services in a database, and sends values _via_ the broker.
|
|
||||||
|
|
||||||
Each item consists in :
|
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 **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 **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)
|
- 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
|
## Available domains
|
||||||
|
|
||||||
- **AD** : ad related configuration items
|
- **CARPOOL** : carpool related configuration items (eg. default number of seats proposed as a driver)
|
||||||
- **MATCHER** : matching algotithm related configuration items
|
- **PAGINATION** : pagination related configuration items (eg. default number of results per page)
|
||||||
- **USER** : user related configuration items
|
|
||||||
|
New domains will be added in the future depending on the needs !
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
@ -57,11 +55,7 @@ A RabbitMQ instance is also required to send / receive messages when data has be
|
||||||
|
|
||||||
## Database migration
|
## Database migration
|
||||||
|
|
||||||
Before using the app, you need to launch the database migration (it will be launched inside the container) :
|
Redis database is automatically populated at the start of the service. If keys already exists in the database, items are preserved.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -71,52 +65,24 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "AD",
|
"domain": "CARPOOL",
|
||||||
"key": "seatsProposed"
|
"key": "seatsProposed"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Set** : create or update a configuration item
|
- **Set** : update a configuration item
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "USER",
|
"domain": "CARPOOL",
|
||||||
"key": "key1",
|
"key": "seatsProposed",
|
||||||
"value": "value1"
|
"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 / 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).
|
Tests are run outside the container for ease of use.
|
||||||
The integration tests use a dedicated database (see _db-test_ section of _docker-compose.yml_).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# run all tests (unit + integration)
|
# run all tests (unit + integration)
|
||||||
|
|
14
ci/.env.ci
14
ci/.env.ci
|
@ -2,14 +2,6 @@
|
||||||
SERVICE_URL=0.0.0.0
|
SERVICE_URL=0.0.0.0
|
||||||
SERVICE_PORT=5003
|
SERVICE_PORT=5003
|
||||||
|
|
||||||
# PRISMA
|
# REDIS
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
REDIS_IMAGE=redis:7.0-alpine
|
||||||
|
REDIS_PASSWORD=redis
|
||||||
# RABBIT MQ
|
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
|
||||||
|
|
||||||
# MESSAGE BROKER
|
|
||||||
BROKER_IMAGE=rabbitmq:3-alpine
|
|
||||||
|
|
||||||
# POSTGRES
|
|
||||||
POSTGRES_IMAGE=postgres:15.0
|
|
||||||
|
|
|
@ -16,9 +16,6 @@ RUN npm ci
|
||||||
# Bundle app source
|
# Bundle app source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Generate prisma client
|
|
||||||
RUN npx prisma generate
|
|
||||||
|
|
||||||
# Create a "dist" folder
|
# Create a "dist" folder
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
testlog() {
|
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
|
testlog 2> /dev/null
|
||||||
while [ $? -ne 0 ];
|
while [ $? -ne 0 ];
|
||||||
do
|
do
|
||||||
sleep 5
|
sleep 5
|
||||||
echo "Waiting for Test DB to be up..."
|
echo "Waiting for Test Redis to be up..."
|
||||||
testlog 2> /dev/null
|
testlog 2> /dev/null
|
||||||
done
|
done
|
||||||
|
|
|
@ -1,23 +1,12 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
redis:
|
||||||
container_name: v3-db
|
container_name: v3-redis
|
||||||
image: ${POSTGRES_IMAGE}
|
image: ${REDIS_IMAGE}
|
||||||
environment:
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
POSTGRES_DB: mobicoop
|
|
||||||
POSTGRES_USER: mobicoop
|
|
||||||
POSTGRES_PASSWORD: mobicoop
|
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 6379:6379
|
||||||
networks:
|
|
||||||
- v3-network
|
|
||||||
|
|
||||||
broker:
|
|
||||||
container_name: v3-broker
|
|
||||||
image: ${BROKER_IMAGE}
|
|
||||||
ports:
|
|
||||||
- 5672:5672
|
|
||||||
networks:
|
networks:
|
||||||
- v3-network
|
- v3-network
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/configuration",
|
"name": "@mobicoop/configuration",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@mobicoop/configuration",
|
"name": "@mobicoop/configuration",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.6",
|
"@grpc/grpc-js": "^1.9.6",
|
||||||
|
@ -23,8 +23,10 @@
|
||||||
"@nestjs/platform-express": "^10.2.7",
|
"@nestjs/platform-express": "^10.2.7",
|
||||||
"@nestjs/terminus": "^10.1.1",
|
"@nestjs/terminus": "^10.1.1",
|
||||||
"@prisma/client": "^5.4.2",
|
"@prisma/client": "^5.4.2",
|
||||||
|
"@songkeys/nestjs-redis": "^10.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -1096,6 +1098,11 @@
|
||||||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
@ -2490,6 +2497,27 @@
|
||||||
"@sinonjs/commons": "^3.0.0"
|
"@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": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||||
|
@ -4098,6 +4126,14 @@
|
||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
|
@ -4581,6 +4617,14 @@
|
||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
@ -5559,20 +5603,6 @@
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
@ -5952,6 +5982,29 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
"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": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
@ -8022,6 +8085,25 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||||
|
@ -8546,6 +8628,11 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
|
19
package.json
19
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/configuration",
|
"name": "@mobicoop/configuration",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Mobicoop V3 Configuration Service",
|
"description": "Mobicoop V3 Configuration Service",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -17,18 +17,11 @@
|
||||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||||
"pretty:check": "./node_modules/.bin/prettier --check .",
|
"pretty:check": "./node_modules/.bin/prettier --check .",
|
||||||
"pretty": "./node_modules/.bin/prettier --write .",
|
"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": "jest --testPathPattern 'tests/unit/' --verbose",
|
||||||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
"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:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.6",
|
"@grpc/grpc-js": "^1.9.6",
|
||||||
|
@ -44,9 +37,10 @@
|
||||||
"@nestjs/microservices": "^10.2.7",
|
"@nestjs/microservices": "^10.2.7",
|
||||||
"@nestjs/platform-express": "^10.2.7",
|
"@nestjs/platform-express": "^10.2.7",
|
||||||
"@nestjs/terminus": "^10.1.1",
|
"@nestjs/terminus": "^10.1.1",
|
||||||
"@prisma/client": "^5.4.2",
|
"@songkeys/nestjs-redis": "^10.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -69,7 +63,6 @@
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"prisma": "^5.4.2",
|
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.1",
|
||||||
|
@ -91,6 +84,7 @@
|
||||||
".di-tokens.ts",
|
".di-tokens.ts",
|
||||||
".response.ts",
|
".response.ts",
|
||||||
".port.ts",
|
".port.ts",
|
||||||
|
".config.ts",
|
||||||
"prisma.service.ts",
|
"prisma.service.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
|
@ -109,6 +103,7 @@
|
||||||
".di-tokens.ts",
|
".di-tokens.ts",
|
||||||
".response.ts",
|
".response.ts",
|
||||||
".port.ts",
|
".port.ts",
|
||||||
|
".config.ts",
|
||||||
"prisma.service.ts",
|
"prisma.service.ts",
|
||||||
"main.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_PACKAGE_NAME = 'configuration';
|
||||||
export const GRPC_SERVICE_NAME = 'ConfigurationService';
|
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
|
// health
|
||||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||||
export const HEALTH_CONFIGURATION_REPOSITORY = 'ConfigurationRepository';
|
|
||||||
export const HEALTH_CRITICAL_LOGGING_KEY = 'logging.configuration.health.crit';
|
export const HEALTH_CRITICAL_LOGGING_KEY = 'logging.configuration.health.crit';
|
||||||
|
|
|
@ -1,44 +1,69 @@
|
||||||
import {
|
import { HealthModule, HealthModuleOptions } from '@mobicoop/health-module';
|
||||||
HealthModule,
|
|
||||||
HealthModuleOptions,
|
|
||||||
HealthRepositoryPort,
|
|
||||||
} from '@mobicoop/health-module';
|
|
||||||
import { MessagerModule } from '@modules/messager/messager.module';
|
import { MessagerModule } from '@modules/messager/messager.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import {
|
import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
|
||||||
HEALTH_CONFIGURATION_REPOSITORY,
|
|
||||||
HEALTH_CRITICAL_LOGGING_KEY,
|
|
||||||
SERVICE_NAME,
|
|
||||||
} from './app.constants';
|
|
||||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
|
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 { ConfigurationModule } from '@modules/configuration/configuration.module';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [
|
||||||
|
brokerConfig,
|
||||||
|
carpoolConfig,
|
||||||
|
paginationConfig,
|
||||||
|
redisConfig,
|
||||||
|
serviceConfig,
|
||||||
|
],
|
||||||
|
}),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
HealthModule.forRootAsync({
|
HealthModule.forRootAsync({
|
||||||
imports: [ConfigurationModule, MessagerModule],
|
imports: [MessagerModule, ConfigModule],
|
||||||
inject: [CONFIGURATION_REPOSITORY, MESSAGE_PUBLISHER],
|
inject: [MESSAGE_PUBLISHER, ConfigService],
|
||||||
useFactory: async (
|
useFactory: async (
|
||||||
configurationRepository: HealthRepositoryPort,
|
|
||||||
messagePublisher: MessagePublisherPort,
|
messagePublisher: MessagePublisherPort,
|
||||||
|
configService: ConfigService,
|
||||||
): Promise<HealthModuleOptions> => ({
|
): Promise<HealthModuleOptions> => ({
|
||||||
serviceName: SERVICE_NAME,
|
serviceName: SERVICE_NAME,
|
||||||
criticalLoggingKey: HEALTH_CRITICAL_LOGGING_KEY,
|
criticalLoggingKey: HEALTH_CRITICAL_LOGGING_KEY,
|
||||||
checkRepositories: [
|
messagePublisher,
|
||||||
|
checkMicroservices: [
|
||||||
{
|
{
|
||||||
name: HEALTH_CONFIGURATION_REPOSITORY,
|
host: configService.get<string>('redis.host') as string,
|
||||||
repository: configurationRepository,
|
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,
|
ConfigurationModule,
|
||||||
MessagerModule,
|
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');
|
export const CONFIGURATION_REPOSITORY = Symbol('CONFIGURATION_REPOSITORY');
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import { Mapper } from '@mobicoop/ddd-library';
|
import { Mapper } from '@mobicoop/ddd-library';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigurationEntity } from './core/domain/configuration.entity';
|
import { ConfigurationEntity } from './core/domain/configuration.entity';
|
||||||
|
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
|
||||||
|
import { ConfigurationDomain } from './core/domain/configuration.types';
|
||||||
import {
|
import {
|
||||||
ConfigurationReadModel,
|
ConfigurationReadModel,
|
||||||
ConfigurationWriteModel,
|
ConfigurationWriteModel,
|
||||||
} from './infrastructure/configuration.repository';
|
} from './infrastructure/configuration.repository';
|
||||||
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
/**
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConfigurationMapper
|
export class ConfigurationMapper
|
||||||
|
@ -27,9 +22,7 @@ export class ConfigurationMapper
|
||||||
toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
|
toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
|
||||||
const copy = entity.getProps();
|
const copy = entity.getProps();
|
||||||
const record: ConfigurationWriteModel = {
|
const record: ConfigurationWriteModel = {
|
||||||
uuid: entity.id,
|
key: `${copy.identifier.domain}:${copy.identifier.key}`,
|
||||||
domain: copy.domain,
|
|
||||||
key: copy.key,
|
|
||||||
value: copy.value,
|
value: copy.value,
|
||||||
};
|
};
|
||||||
return record;
|
return record;
|
||||||
|
@ -37,12 +30,14 @@ export class ConfigurationMapper
|
||||||
|
|
||||||
toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
|
toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
|
||||||
const entity = new ConfigurationEntity({
|
const entity = new ConfigurationEntity({
|
||||||
id: record.uuid,
|
id: v4(),
|
||||||
createdAt: new Date(record.createdAt),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(record.updatedAt),
|
updatedAt: new Date(),
|
||||||
props: {
|
props: {
|
||||||
domain: record.domain,
|
identifier: {
|
||||||
key: record.key,
|
domain: record.key.split(':')[0] as ConfigurationDomain,
|
||||||
|
key: record.key.split(':')[1],
|
||||||
|
},
|
||||||
value: record.value,
|
value: record.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -52,8 +47,8 @@ export class ConfigurationMapper
|
||||||
toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
|
toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
|
||||||
const props = entity.getProps();
|
const props = entity.getProps();
|
||||||
const response = new ConfigurationResponseDto(entity);
|
const response = new ConfigurationResponseDto(entity);
|
||||||
response.domain = props.domain;
|
response.domain = props.identifier.domain;
|
||||||
response.key = props.key;
|
response.key = props.identifier.key;
|
||||||
response.value = props.value;
|
response.value = props.value;
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,45 +2,26 @@ import { Module, Provider } from '@nestjs/common';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller';
|
import { GetConfigurationGrpcController } from './interface/grpc-controllers/get-configuration.grpc.controller';
|
||||||
import { SetConfigurationGrpcController } from './interface/grpc-controllers/set-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 { 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 { GetConfigurationQueryHandler } from './core/application/queries/get-configuration/get-configuration.query-handler';
|
||||||
import { ConfigurationMapper } from './configuration.mapper';
|
import { ConfigurationMapper } from './configuration.mapper';
|
||||||
import {
|
import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens';
|
||||||
CONFIGURATION_MESSAGE_PUBLISHER,
|
import { PopulateService } from './core/application/services/populate.service';
|
||||||
CONFIGURATION_REPOSITORY,
|
|
||||||
} from './configuration.di-tokens';
|
|
||||||
import { ConfigurationRepository } from './infrastructure/configuration.repository';
|
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 = [
|
const grpcControllers = [
|
||||||
GetConfigurationGrpcController,
|
GetConfigurationGrpcController,
|
||||||
SetConfigurationGrpcController,
|
SetConfigurationGrpcController,
|
||||||
DeleteConfigurationGrpcController,
|
|
||||||
PropagateConfigurationsGrpcController,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventHandlers: Provider[] = [
|
const commandHandlers: Provider[] = [SetConfigurationService];
|
||||||
PublishMessageWhenConfigurationIsSetDomainEventHandler,
|
|
||||||
PublishMessageWhenConfigurationIsDeletedDomainEventHandler,
|
|
||||||
];
|
|
||||||
|
|
||||||
const commandHandlers: Provider[] = [
|
|
||||||
SetConfigurationService,
|
|
||||||
DeleteConfigurationService,
|
|
||||||
PropagateConfigurationsService,
|
|
||||||
];
|
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [GetConfigurationQueryHandler];
|
const queryHandlers: Provider[] = [GetConfigurationQueryHandler];
|
||||||
|
|
||||||
const mappers: Provider[] = [ConfigurationMapper];
|
const mappers: Provider[] = [ConfigurationMapper];
|
||||||
|
|
||||||
|
const providers: Provider[] = [PopulateService];
|
||||||
|
|
||||||
const repositories: Provider[] = [
|
const repositories: Provider[] = [
|
||||||
{
|
{
|
||||||
provide: CONFIGURATION_REPOSITORY,
|
provide: CONFIGURATION_REPOSITORY,
|
||||||
|
@ -48,26 +29,16 @@ const repositories: Provider[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const messagePublishers: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: CONFIGURATION_MESSAGE_PUBLISHER,
|
|
||||||
useExisting: MessageBrokerPublisher,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const orms: Provider[] = [PrismaService];
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule],
|
imports: [CqrsModule],
|
||||||
controllers: [...grpcControllers],
|
controllers: [...grpcControllers],
|
||||||
providers: [
|
providers: [
|
||||||
...eventHandlers,
|
|
||||||
...commandHandlers,
|
...commandHandlers,
|
||||||
...queryHandlers,
|
...queryHandlers,
|
||||||
...mappers,
|
...mappers,
|
||||||
|
...providers,
|
||||||
...repositories,
|
...repositories,
|
||||||
...messagePublishers,
|
|
||||||
...orms,
|
|
||||||
],
|
],
|
||||||
exports: [PrismaService, ConfigurationMapper, CONFIGURATION_REPOSITORY],
|
exports: [ConfigurationMapper, CONFIGURATION_REPOSITORY],
|
||||||
})
|
})
|
||||||
export class ConfigurationModule {}
|
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 { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
|
||||||
import { SetConfigurationCommand } from './set-configuration.command';
|
import { SetConfigurationCommand } from './set-configuration.command';
|
||||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
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 { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
|
||||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
|
||||||
|
|
||||||
@CommandHandler(SetConfigurationCommand)
|
@CommandHandler(SetConfigurationCommand)
|
||||||
export class SetConfigurationService implements ICommandHandler {
|
export class SetConfigurationService implements ICommandHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(CONFIGURATION_REPOSITORY)
|
@Inject(CONFIGURATION_REPOSITORY)
|
||||||
private readonly repository: ConfigurationRepositoryPort,
|
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: SetConfigurationCommand): Promise<AggregateID> {
|
async execute(command: SetConfigurationCommand): Promise<void> {
|
||||||
try {
|
await this.configurationRepository.set(
|
||||||
const existingConfiguration: ConfigurationEntity =
|
{
|
||||||
await this.repository.findOne({
|
domain: command.domain as ConfigurationDomain,
|
||||||
domain: command.domain,
|
key: command.key,
|
||||||
key: command.key,
|
},
|
||||||
});
|
command.value,
|
||||||
existingConfiguration.update(command);
|
);
|
||||||
await this.repository.update(
|
|
||||||
existingConfiguration.id,
|
|
||||||
existingConfiguration,
|
|
||||||
);
|
|
||||||
return existingConfiguration.id;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof NotFoundException) {
|
|
||||||
try {
|
|
||||||
const newConfiguration = ConfigurationEntity.create(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 { 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 { GetConfigurationQuery } from './get-configuration.query';
|
||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
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 { 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)
|
@QueryHandler(GetConfigurationQuery)
|
||||||
export class GetConfigurationQueryHandler implements IQueryHandler {
|
export class GetConfigurationQueryHandler implements IQueryHandler {
|
||||||
|
@ -12,6 +13,9 @@ export class GetConfigurationQueryHandler implements IQueryHandler {
|
||||||
private readonly configurationRepository: ConfigurationRepositoryPort,
|
private readonly configurationRepository: ConfigurationRepositoryPort,
|
||||||
) {}
|
) {}
|
||||||
async execute(query: GetConfigurationQuery): Promise<ConfigurationEntity> {
|
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 {
|
import {
|
||||||
ConfigurationProps,
|
ConfigurationProps,
|
||||||
CreateConfigurationProps,
|
CreateConfigurationProps,
|
||||||
UpdateConfigurationProps,
|
|
||||||
} from './configuration.types';
|
} 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> {
|
export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
|
||||||
protected readonly _id: AggregateID;
|
protected readonly _id: AggregateID;
|
||||||
|
@ -15,39 +12,9 @@ export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
|
||||||
const id = v4();
|
const id = v4();
|
||||||
const props: ConfigurationProps = { ...create };
|
const props: ConfigurationProps = { ...create };
|
||||||
const configuration = new ConfigurationEntity({ id, props });
|
const configuration = new ConfigurationEntity({ id, props });
|
||||||
configuration.addEvent(
|
|
||||||
new ConfigurationSetDomainEvent({
|
|
||||||
aggregateId: id,
|
|
||||||
domain: props.domain,
|
|
||||||
key: props.key,
|
|
||||||
value: props.value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return configuration;
|
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 {
|
validate(): void {
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
// 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
|
// All properties that a Configuration has
|
||||||
export interface ConfigurationProps {
|
export interface ConfigurationProps {
|
||||||
domain: string;
|
identifier: ConfigurationIdentifier;
|
||||||
key: string;
|
value: ConfigurationValue;
|
||||||
value: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Properties that are needed for a Configuration creation
|
// Properties that are needed for a Configuration creation
|
||||||
export interface CreateConfigurationProps {
|
export interface CreateConfigurationProps {
|
||||||
domain: string;
|
identifier: ConfigurationIdentifier;
|
||||||
key: string;
|
value: ConfigurationValue;
|
||||||
value: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateConfigurationProps {
|
export enum ConfigurationDomain {
|
||||||
value: string;
|
CARPOOL = 'CARPOOL',
|
||||||
|
PAGINATION = 'PAGINATION',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Domain {
|
export type ConfigurationIdentifier = {
|
||||||
AD = 'AD',
|
domain: ConfigurationDomain;
|
||||||
MATCHER = 'MATCHER',
|
key: ConfigurationKey;
|
||||||
USER = 'USER',
|
};
|
||||||
|
|
||||||
|
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 { Injectable } 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 { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
|
import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
|
||||||
import { PrismaService } from './prisma.service';
|
import { InjectRedis } from '@songkeys/nestjs-redis';
|
||||||
import { CONFIGURATION_MESSAGE_PUBLISHER } from '../configuration.di-tokens';
|
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 { ConfigurationMapper } from '../configuration.mapper';
|
||||||
|
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
export type ConfigurationBaseModel = {
|
export type ConfigurationReadModel = {
|
||||||
uuid: string;
|
|
||||||
domain: string;
|
|
||||||
key: string;
|
key: string;
|
||||||
value: 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()
|
@Injectable()
|
||||||
export class ConfigurationRepository
|
export class ConfigurationRepository implements ConfigurationRepositoryPort {
|
||||||
extends PrismaRepositoryBase<
|
|
||||||
ConfigurationEntity,
|
|
||||||
ConfigurationReadModel,
|
|
||||||
ConfigurationWriteModel
|
|
||||||
>
|
|
||||||
implements ConfigurationRepositoryPort
|
|
||||||
{
|
|
||||||
constructor(
|
constructor(
|
||||||
prisma: PrismaService,
|
@InjectRedis() private readonly redis: Redis,
|
||||||
mapper: ConfigurationMapper,
|
private readonly mapper: ConfigurationMapper,
|
||||||
eventEmitter: EventEmitter2,
|
) {}
|
||||||
@Inject(CONFIGURATION_MESSAGE_PUBLISHER)
|
|
||||||
protected readonly messagePublisher: MessagePublisherPort,
|
get = async (
|
||||||
) {
|
identifier: ConfigurationIdentifier,
|
||||||
super(
|
): Promise<ConfigurationEntity> => {
|
||||||
prisma.configuration,
|
const key: string = `${identifier.domain}:${identifier.key}`;
|
||||||
prisma,
|
const value: ConfigurationValue | null = await this.redis.get(key);
|
||||||
mapper,
|
if (!value)
|
||||||
eventEmitter,
|
throw new NotFoundException(
|
||||||
new LoggerBase({
|
`Configuration item not found for key ${key}`,
|
||||||
logger: new Logger(ConfigurationRepository.name),
|
);
|
||||||
domain: SERVICE_NAME,
|
return this.mapper.toDomain({
|
||||||
messagePublisher,
|
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 {
|
service ConfigurationService {
|
||||||
rpc Get(ConfigurationByDomainKey) returns (Configuration);
|
rpc Get(ConfigurationByDomainKey) returns (Configuration);
|
||||||
rpc Set(Configuration) returns (ConfigurationId);
|
rpc Set(Configuration) returns (Empty);
|
||||||
rpc Delete(ConfigurationByDomainKey) returns (Empty);
|
|
||||||
rpc Propagate(Empty) returns (Empty);
|
|
||||||
}
|
|
||||||
message ConfigurationId {
|
|
||||||
string id = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ConfigurationByDomainKey {
|
message ConfigurationByDomainKey {
|
||||||
string domain = 1;
|
string domain = 1;
|
||||||
string key = 2;
|
string key = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Configuration {
|
message Configuration {
|
||||||
string id = 1;
|
string domain = 1;
|
||||||
string domain = 2;
|
string key = 2;
|
||||||
string key = 3;
|
string value = 3;
|
||||||
string value = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Empty {}
|
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';
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class GetConfigurationRequestDto {
|
export class GetConfigurationRequestDto {
|
||||||
@IsEnum(Domain)
|
@IsEnum(ConfigurationDomain)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
domain: Domain;
|
domain: ConfigurationDomain;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@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';
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class SetConfigurationRequestDto {
|
export class SetConfigurationRequestDto {
|
||||||
@IsEnum(Domain)
|
@IsEnum(ConfigurationDomain)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
domain: Domain;
|
domain: ConfigurationDomain;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@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 { Controller, UsePipes } from '@nestjs/common';
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
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 { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||||
|
@ -22,12 +20,11 @@ export class SetConfigurationGrpcController {
|
||||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Set')
|
@GrpcMethod(GRPC_SERVICE_NAME, 'Set')
|
||||||
async set(
|
async set(
|
||||||
setConfigurationRequestDto: SetConfigurationRequestDto,
|
setConfigurationRequestDto: SetConfigurationRequestDto,
|
||||||
): Promise<IdResponse> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
await this.commandBus.execute(
|
||||||
new SetConfigurationCommand(setConfigurationRequestDto),
|
new SetConfigurationCommand(setConfigurationRequestDto),
|
||||||
);
|
);
|
||||||
return new IdResponse(aggregateID);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
code: RpcExceptionCode.UNKNOWN,
|
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 { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
||||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
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 {
|
import {
|
||||||
ConfigurationReadModel,
|
ConfigurationReadModel,
|
||||||
ConfigurationWriteModel,
|
ConfigurationWriteModel,
|
||||||
|
@ -12,20 +12,18 @@ const now = new Date('2023-06-21 06:00:00');
|
||||||
const configurationEntity: ConfigurationEntity = new ConfigurationEntity({
|
const configurationEntity: ConfigurationEntity = new ConfigurationEntity({
|
||||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
props: {
|
props: {
|
||||||
domain: Domain.AD,
|
identifier: {
|
||||||
key: 'seatsProposed',
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
},
|
||||||
value: '3',
|
value: '3',
|
||||||
},
|
},
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
const configurationReadModel: ConfigurationReadModel = {
|
const configurationReadModel: ConfigurationReadModel = {
|
||||||
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
key: 'AD:seatsProposed',
|
||||||
domain: 'AD',
|
|
||||||
key: 'seatsProposed',
|
|
||||||
value: '4',
|
value: '4',
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Configuration Mapper', () => {
|
describe('Configuration Mapper', () => {
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||||
import {
|
import {
|
||||||
CreateConfigurationProps,
|
CreateConfigurationProps,
|
||||||
Domain,
|
ConfigurationDomain,
|
||||||
UpdateConfigurationProps,
|
|
||||||
} from '@modules/configuration/core/domain/configuration.types';
|
} 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 = {
|
const createConfigurationProps: CreateConfigurationProps = {
|
||||||
domain: Domain.AD,
|
identifier: {
|
||||||
key: 'seatsProposed',
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
},
|
||||||
value: '3',
|
value: '3',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigurationProps: UpdateConfigurationProps = {
|
|
||||||
value: '2',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Configuration entity create', () => {
|
describe('Configuration entity create', () => {
|
||||||
it('should create a new configuration entity', async () => {
|
it('should create a new configuration entity', async () => {
|
||||||
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
|
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
|
||||||
|
@ -24,38 +19,5 @@ describe('Configuration entity create', () => {
|
||||||
);
|
);
|
||||||
expect(configurationEntity.id.length).toBe(36);
|
expect(configurationEntity.id.length).toBe(36);
|
||||||
expect(configurationEntity.getProps().value).toBe('3');
|
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 { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
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 { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler';
|
||||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||||
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
|
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({
|
const configuration: ConfigurationEntity = new ConfigurationEntity({
|
||||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
props: {
|
props: {
|
||||||
domain: Domain.AD,
|
identifier: {
|
||||||
key: 'seatsProposed',
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
},
|
||||||
value: '3',
|
value: '3',
|
||||||
},
|
},
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
@ -18,7 +20,7 @@ const configuration: ConfigurationEntity = new ConfigurationEntity({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockConfigurationRepository = {
|
const mockConfigurationRepository = {
|
||||||
findOne: jest.fn().mockImplementation(() => configuration),
|
get: jest.fn().mockImplementation(() => configuration),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Get Configuration Query Handler', () => {
|
describe('Get Configuration Query Handler', () => {
|
||||||
|
@ -47,7 +49,7 @@ describe('Get Configuration Query Handler', () => {
|
||||||
describe('execution', () => {
|
describe('execution', () => {
|
||||||
it('should return a configuration item', async () => {
|
it('should return a configuration item', async () => {
|
||||||
const getConfigurationQuery = new GetConfigurationQuery(
|
const getConfigurationQuery = new GetConfigurationQuery(
|
||||||
Domain.AD,
|
ConfigurationDomain.CARPOOL,
|
||||||
'seatsProposed',
|
'seatsProposed',
|
||||||
);
|
);
|
||||||
const configuration: ConfigurationEntity =
|
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 { 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 { 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 { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service';
|
||||||
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
|
||||||
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
|
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
|
||||||
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
|
||||||
|
|
||||||
const setConfigurationRequest: SetConfigurationRequestDto = {
|
const setConfigurationRequest: SetConfigurationRequestDto = {
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'seatsProposed',
|
key: 'seatsProposed',
|
||||||
value: '3',
|
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 = {
|
const mockConfigurationRepository = {
|
||||||
findOne: jest
|
set: jest
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new NotFoundException();
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new NotFoundException();
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => existingConfigurationEntity)
|
|
||||||
.mockImplementationOnce(() => existingConfigurationEntity),
|
|
||||||
insert: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => ({}))
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error();
|
|
||||||
}),
|
|
||||||
update: jest
|
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => ({}))
|
.mockImplementationOnce(() => ({}))
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
|
@ -76,31 +48,13 @@ describe('Set Configuration Service', () => {
|
||||||
const setConfigurationCommand = new SetConfigurationCommand(
|
const setConfigurationCommand = new SetConfigurationCommand(
|
||||||
setConfigurationRequest,
|
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({
|
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
});
|
});
|
||||||
const result: AggregateID = await setConfigurationService.execute(
|
await setConfigurationService.execute(setConfigurationCommand);
|
||||||
setConfigurationCommand,
|
expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1);
|
||||||
);
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
it('should throw an error if something bad happens on configuration item update', async () => {
|
it('should throw an error if something bad happens on configuration item update', async () => {
|
||||||
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
ConfigurationEntity.create = jest.fn().mockReturnValue({
|
||||||
|
|
|
@ -1,36 +1,93 @@
|
||||||
|
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
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 { 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 { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRedisToken } from '@songkeys/nestjs-redis';
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
const mockRedis = {
|
||||||
publish: jest.fn().mockImplementation(),
|
get: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => '1')
|
||||||
|
.mockImplementation(() => null),
|
||||||
|
set: jest.fn().mockImplementation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Configuration repository', () => {
|
const mockConfigurationMapper = {
|
||||||
let prismaService: PrismaService;
|
toDomain: jest.fn().mockImplementation(
|
||||||
let configurationMapper: ConfigurationMapper;
|
() =>
|
||||||
let eventEmitter: EventEmitter2;
|
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 () => {
|
beforeAll(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [EventEmitterModule.forRoot()],
|
providers: [
|
||||||
providers: [PrismaService, ConfigurationMapper],
|
{
|
||||||
|
provide: getRedisToken('default'),
|
||||||
|
useValue: mockRedis,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ConfigurationMapper,
|
||||||
|
useValue: mockConfigurationMapper,
|
||||||
|
},
|
||||||
|
ConfigurationRepository,
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
configurationRepository = module.get<ConfigurationRepository>(
|
||||||
configurationMapper = module.get<ConfigurationMapper>(ConfigurationMapper);
|
ConfigurationRepository,
|
||||||
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(
|
expect(configurationRepository).toBeDefined();
|
||||||
new ConfigurationRepository(
|
});
|
||||||
prismaService,
|
|
||||||
configurationMapper,
|
describe('interact', () => {
|
||||||
eventEmitter,
|
it('should get a value', async () => {
|
||||||
mockMessagePublisher,
|
expect(
|
||||||
),
|
(
|
||||||
).toBeDefined();
|
await configurationRepository.get({
|
||||||
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
})
|
||||||
|
).getProps().value,
|
||||||
|
).toBe('1');
|
||||||
|
});
|
||||||
|
it('should throw if configuration is not found', async () => {
|
||||||
|
await expect(
|
||||||
|
configurationRepository.get({
|
||||||
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
it('should set a value', async () => {
|
||||||
|
expect(
|
||||||
|
await configurationRepository.set(
|
||||||
|
{
|
||||||
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
|
key: 'seatsProposed',
|
||||||
|
},
|
||||||
|
'3',
|
||||||
|
),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { NotFoundException } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
|
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 { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller';
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
import { RpcException } from '@nestjs/microservices';
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
@ -21,7 +21,7 @@ const mockQueryBus = {
|
||||||
|
|
||||||
const mockConfigurationMapper = {
|
const mockConfigurationMapper = {
|
||||||
toResponse: jest.fn().mockImplementationOnce(() => ({
|
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'seatsProposed',
|
key: 'seatsProposed',
|
||||||
value: '3',
|
value: '3',
|
||||||
})),
|
})),
|
||||||
|
@ -62,7 +62,7 @@ describe('Get Configuration Grpc Controller', () => {
|
||||||
jest.spyOn(mockQueryBus, 'execute');
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
jest.spyOn(mockConfigurationMapper, 'toResponse');
|
jest.spyOn(mockConfigurationMapper, 'toResponse');
|
||||||
const response = await getConfigurationGrpcController.get({
|
const response = await getConfigurationGrpcController.get({
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'seatsProposed',
|
key: 'seatsProposed',
|
||||||
});
|
});
|
||||||
expect(response.value).toBe('3');
|
expect(response.value).toBe('3');
|
||||||
|
@ -76,7 +76,7 @@ describe('Get Configuration Grpc Controller', () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
try {
|
try {
|
||||||
await getConfigurationGrpcController.get({
|
await getConfigurationGrpcController.get({
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'price',
|
key: 'price',
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -93,7 +93,7 @@ describe('Get Configuration Grpc Controller', () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
try {
|
try {
|
||||||
await getConfigurationGrpcController.get({
|
await getConfigurationGrpcController.get({
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'someValue',
|
key: 'someValue',
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} 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 { 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 { 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 { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
@ -8,7 +7,7 @@ import { RpcException } from '@nestjs/microservices';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
const setConfigurationRequest: SetConfigurationRequestDto = {
|
const setConfigurationRequest: SetConfigurationRequestDto = {
|
||||||
domain: Domain.AD,
|
domain: ConfigurationDomain.CARPOOL,
|
||||||
key: 'seatsProposed',
|
key: 'seatsProposed',
|
||||||
value: '3',
|
value: '3',
|
||||||
};
|
};
|
||||||
|
@ -51,11 +50,7 @@ describe('Set Configuration Grpc Controller', () => {
|
||||||
|
|
||||||
it('should set a configuration item', async () => {
|
it('should set a configuration item', async () => {
|
||||||
jest.spyOn(mockCommandBus, 'execute');
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
const result: IdResponse = await setConfigurationGrpcController.set(
|
await setConfigurationGrpcController.set(setConfigurationRequest);
|
||||||
setConfigurationRequest,
|
|
||||||
);
|
|
||||||
expect(result).toBeInstanceOf(IdResponse);
|
|
||||||
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
|
||||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,10 @@ const imports = [
|
||||||
useFactory: async (
|
useFactory: async (
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
): Promise<MessageBrokerModuleOptions> => ({
|
): Promise<MessageBrokerModuleOptions> => ({
|
||||||
uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
|
uri: configService.get<string>('broker.uri') as string,
|
||||||
exchange: {
|
exchange: {
|
||||||
name: configService.get<string>('MESSAGE_BROKER_EXCHANGE') as string,
|
name: configService.get<string>('broker.exchange') as string,
|
||||||
durable: configService.get<boolean>(
|
durable: configService.get<boolean>('broker.durability') as boolean,
|
||||||
'MESSAGE_BROKER_EXCHANGE_DURABILITY',
|
|
||||||
) as boolean,
|
|
||||||
},
|
},
|
||||||
name: SERVICE_NAME,
|
name: SERVICE_NAME,
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in New Issue