Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
|
0a92d93949 | |
|
193e613ad6 | |
|
ed1498922c | |
|
01723a94dc | |
|
c046e6c53a | |
|
c137a3b65e | |
|
f096e374f8 | |
|
8ce5f0ca13 | |
|
b80cc37c13 | |
|
15b8cb0819 | |
|
6693a5758f | |
|
c53824af47 | |
|
4a4c1046a0 | |
|
852f5979f4 | |
|
30d4c807e9 | |
|
3f4236f34f | |
|
ad8b98da40 |
|
@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6001
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Feature Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Documentation reflects the changes made.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
- [ ] One or more reviewer is defined
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code follows project coding guidelines.
|
||||||
|
- [ ] Code follows project designed architecture.
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
|
||||||
|
- [ ] No redundant/duplicate code (unless explain by architecture choice)
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] New code is tested and covered by automated test.
|
||||||
|
- [ ] No useless logging or debugging code.
|
||||||
|
- [ ] No code can be replaced by library or framework code.
|
||||||
|
|
||||||
|
### TODO before merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
### TODO after merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
|
@ -0,0 +1,62 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Release Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Documentation reflects the changes made.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
- [ ] One or more reviewer is defined
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code follows project coding guidelines.
|
||||||
|
- [ ] Code follows project designed architecture.
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
|
||||||
|
- [ ] No redundant/duplicate code (unless explain by architecture choice)
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] New code is tested and covered by automated test.
|
||||||
|
- [ ] No useless logging or debugging code.
|
||||||
|
- [ ] No code can be replaced by library or framework code.
|
||||||
|
|
||||||
|
### Change Management
|
||||||
|
|
||||||
|
- [ ] Release is planned
|
||||||
|
- [ ] Merge Request to be included are identified
|
||||||
|
- [ ] Concerned Team are aware of the change
|
||||||
|
- [ ] No other change on the same day (if possible)
|
||||||
|
|
||||||
|
### TODO before merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
### TODO after merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
|
@ -0,0 +1,37 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Small Fix Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] No useless logging or debugging code.
|
|
@ -4,3 +4,4 @@ node_modules
|
||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
.prettierrc.json
|
.prettierrc.json
|
||||||
|
.gitlab
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BUILD FOR LOCAL DEVELOPMENT
|
# BUILD FOR LOCAL DEVELOPMENT
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As development
|
FROM docker.io/node:lts-hydrogen As development
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
@ -29,7 +29,7 @@ USER node
|
||||||
# BUILD FOR PRODUCTION
|
# BUILD FOR PRODUCTION
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As build
|
FROM docker.io/node:lts-hydrogen As build
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ USER node
|
||||||
# PRODUCTION
|
# PRODUCTION
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As production
|
FROM docker.io/node:lts-hydrogen As production
|
||||||
|
|
||||||
# Copy package.json to be able to execute migration command
|
# Copy package.json to be able to execute migration command
|
||||||
COPY --chown=node:node package*.json ./
|
COPY --chown=node:node package*.json ./
|
||||||
|
|
12
README.md
12
README.md
|
@ -56,6 +56,18 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **FindAllByIds** : find all users for the given ids
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
"80126a61-d128-4f96-afdb-92e33c75a3e1",
|
||||||
|
"80126a61-d128-4f96-afdb-92e33c75a3e2",
|
||||||
|
"80126a61-d128-4f96-afdb-92e33c75a3e3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **Create** : create a user
|
- **Create** : create a user
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -6,10 +6,10 @@ SERVICE_PORT=5001
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
||||||
|
|
||||||
# RABBIT MQ
|
# RABBIT MQ
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
RMQ_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
BROKER_IMAGE=rabbitmq:3-alpine
|
BROKER_IMAGE=docker.io/rabbitmq:3-alpine
|
||||||
|
|
||||||
# POSTGRES
|
# POSTGRES
|
||||||
POSTGRES_IMAGE=postgres:15.0
|
POSTGRES_IMAGE=docker.io/postgres:15.0
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BUILD FOR CI TESTING
|
# BUILD FOR CI TESTING
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16
|
FROM docker.io/node:lts-hydrogen
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/user",
|
"name": "@mobicoop/user",
|
||||||
"version": "1.4.0",
|
"version": "1.5.2",
|
||||||
"description": "Mobicoop V3 User Service",
|
"description": "Mobicoop V3 User Service",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -24,59 +24,60 @@
|
||||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||||
"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",
|
||||||
"migrate": "docker exec v3-user-api sh -c 'npx prisma migrate dev'",
|
"repl": "docker exec -it v3-user-api npm run start -- --entryFile repl",
|
||||||
|
"migrate": "docker exec v3-user-api sh -c 'npx prisma migrate deploy'",
|
||||||
|
"migrate:dev": "docker exec v3-user-api sh -c 'npx prisma migrate dev'",
|
||||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.6",
|
"@grpc/grpc-js": "^1.9.13",
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@songkeys/nestjs-redis": "^10.0.0",
|
"@songkeys/nestjs-redis": "^10.0.0",
|
||||||
"@mobicoop/configuration-module": "^3.0.0",
|
"@mobicoop/ddd-library": "^2.4.3",
|
||||||
"@mobicoop/ddd-library": "^2.0.0",
|
|
||||||
"@mobicoop/health-module": "^2.3.1",
|
"@mobicoop/health-module": "^2.3.1",
|
||||||
"@mobicoop/message-broker-module": "^2.1.1",
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/cache-manager": "^2.1.0",
|
"@nestjs/cache-manager": "^2.2.0",
|
||||||
"@nestjs/common": "^10.2.7",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.2.7",
|
"@nestjs/core": "^10.3.0",
|
||||||
"@nestjs/cqrs": "^10.2.6",
|
"@nestjs/cqrs": "^10.2.6",
|
||||||
"@nestjs/event-emitter": "^2.0.2",
|
"@nestjs/event-emitter": "^2.0.3",
|
||||||
"@nestjs/microservices": "^10.2.7",
|
"@nestjs/microservices": "^10.3.0",
|
||||||
"@nestjs/platform-express": "^10.2.7",
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
"@nestjs/terminus": "^10.1.1",
|
"@nestjs/terminus": "^10.2.0",
|
||||||
"@prisma/client": "^5.4.2",
|
"@prisma/client": "^5.8.1",
|
||||||
"@types/supertest": "^2.0.14",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||||
"@typescript-eslint/parser": "^6.8.0",
|
"@typescript-eslint/parser": "^6.19.0",
|
||||||
"cache-manager": "^5.2.4",
|
"cache-manager": "^5.3.2",
|
||||||
"cache-manager-ioredis-yet": "^1.2.2",
|
"cache-manager-ioredis-yet": "^1.2.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^7.3.0",
|
||||||
"ioredis": "^5.3.2"
|
"ioredis": "^5.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.1.18",
|
"@nestjs/cli": "^10.3.0",
|
||||||
"@nestjs/schematics": "^10.0.2",
|
"@nestjs/schematics": "^10.1.0",
|
||||||
"@nestjs/testing": "^10.2.7",
|
"@nestjs/testing": "^10.3.0",
|
||||||
"@types/jest": "^29.5.6",
|
"@types/jest": "^29.5.11",
|
||||||
"@types/node": "^20.8.6",
|
"@types/node": "^20.11.4",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^9.0.7",
|
||||||
"eslint": "^8.51.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.2.2",
|
||||||
"prisma": "^5.4.2",
|
"prisma": "^5.8.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.4",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.1",
|
||||||
"ts-loader": "^9.5.0",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|
|
@ -5,12 +5,6 @@ export const SERVICE_NAME = 'user';
|
||||||
export const GRPC_PACKAGE_NAME = 'user';
|
export const GRPC_PACKAGE_NAME = 'user';
|
||||||
export const GRPC_SERVICE_NAME = 'UserService';
|
export const GRPC_SERVICE_NAME = 'UserService';
|
||||||
|
|
||||||
// configuration
|
|
||||||
export const SERVICE_CONFIGURATION_SET_QUEUE = 'user-configuration-set';
|
|
||||||
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'user-configuration-delete';
|
|
||||||
export const SERVICE_CONFIGURATION_PROPAGATE_QUEUE =
|
|
||||||
'user-configuration-propagate';
|
|
||||||
|
|
||||||
// health
|
// health
|
||||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||||
export const HEALTH_USER_REPOSITORY = 'UserRepository';
|
export const HEALTH_USER_REPOSITORY = 'UserRepository';
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { UserModule } from './modules/user/user.module';
|
import { UserModule } from './modules/user/user.module';
|
||||||
import {
|
|
||||||
ConfigurationModule,
|
|
||||||
ConfigurationModuleOptions,
|
|
||||||
} from '@mobicoop/configuration-module';
|
|
||||||
import {
|
import {
|
||||||
HealthModule,
|
HealthModule,
|
||||||
HealthModuleOptions,
|
HealthModuleOptions,
|
||||||
|
@ -18,9 +14,6 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import {
|
import {
|
||||||
HEALTH_CRITICAL_LOGGING_KEY,
|
HEALTH_CRITICAL_LOGGING_KEY,
|
||||||
HEALTH_USER_REPOSITORY,
|
HEALTH_USER_REPOSITORY,
|
||||||
SERVICE_CONFIGURATION_DELETE_QUEUE,
|
|
||||||
SERVICE_CONFIGURATION_PROPAGATE_QUEUE,
|
|
||||||
SERVICE_CONFIGURATION_SET_QUEUE,
|
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
} from './app.constants';
|
} from './app.constants';
|
||||||
|
|
||||||
|
@ -28,36 +21,6 @@ import {
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
ConfigurationModule.forRootAsync({
|
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (
|
|
||||||
configService: ConfigService,
|
|
||||||
): Promise<ConfigurationModuleOptions> => ({
|
|
||||||
domain: configService.get<string>(
|
|
||||||
'SERVICE_CONFIGURATION_DOMAIN',
|
|
||||||
) as string,
|
|
||||||
messageBroker: {
|
|
||||||
uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
|
|
||||||
exchange: {
|
|
||||||
name: configService.get<string>(
|
|
||||||
'MESSAGE_BROKER_EXCHANGE',
|
|
||||||
) as string,
|
|
||||||
durable: configService.get<boolean>(
|
|
||||||
'MESSAGE_BROKER_EXCHANGE',
|
|
||||||
) as boolean,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
host: configService.get<string>('REDIS_HOST') as string,
|
|
||||||
password: configService.get<string>('REDIS_PASSWORD'),
|
|
||||||
port: configService.get<number>('REDIS_PORT') as number,
|
|
||||||
},
|
|
||||||
setConfigurationQueue: SERVICE_CONFIGURATION_SET_QUEUE,
|
|
||||||
deleteConfigurationQueue: SERVICE_CONFIGURATION_DELETE_QUEUE,
|
|
||||||
propagateConfigurationQueue: SERVICE_CONFIGURATION_PROPAGATE_QUEUE,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
HealthModule.forRootAsync({
|
HealthModule.forRootAsync({
|
||||||
imports: [UserModule, MessagerModule],
|
imports: [UserModule, MessagerModule],
|
||||||
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],
|
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||||
|
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { FindUsersByIdsQuery } from './find-users-by-ids.query';
|
||||||
|
|
||||||
|
@QueryHandler(FindUsersByIdsQuery)
|
||||||
|
export class FindUsersByIdsQueryHandler implements IQueryHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(USER_REPOSITORY)
|
||||||
|
private readonly userRepository: UserRepositoryPort,
|
||||||
|
) {}
|
||||||
|
async execute(query: FindUsersByIdsQuery): Promise<UserEntity[]> {
|
||||||
|
return await this.userRepository.findAllByIds(query.ids);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class FindUsersByIdsQuery extends QueryBase {
|
||||||
|
readonly ids: string[];
|
||||||
|
|
||||||
|
constructor(ids: string[]) {
|
||||||
|
super();
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { UserResponseDto } from './user.response.dto';
|
||||||
|
|
||||||
|
export class UsersResponseDto {
|
||||||
|
readonly users: readonly UserResponseDto[];
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ArrayMinSize, IsArray } from 'class-validator';
|
||||||
|
|
||||||
|
export class FindUsersByIdsRequestDto {
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
ids: string[];
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
|
import { UserMapper } from '@modules/user/user.mapper';
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||||
|
import { FindUsersByIdsRequestDto } from './dtos/find-users-by-ids.request.dto';
|
||||||
|
import { FindUsersByIdsQuery } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query';
|
||||||
|
import { UsersResponseDto } from '../dtos/users.response.dto';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: false,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class FindUsersByIdsGrpcController {
|
||||||
|
constructor(
|
||||||
|
protected readonly mapper: UserMapper,
|
||||||
|
private readonly queryBus: QueryBus,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByIds')
|
||||||
|
async findAllbyIds(
|
||||||
|
data: FindUsersByIdsRequestDto,
|
||||||
|
): Promise<UsersResponseDto> {
|
||||||
|
try {
|
||||||
|
const users: UserEntity[] = await this.queryBus.execute(
|
||||||
|
new FindUsersByIdsQuery(data.ids),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
users: users.map((user: UserEntity) => this.mapper.toResponse(user)),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ package user;
|
||||||
|
|
||||||
service UserService {
|
service UserService {
|
||||||
rpc FindOneById(UserById) returns (User);
|
rpc FindOneById(UserById) returns (User);
|
||||||
rpc FindAll(UserFilter) returns (Users);
|
rpc FindAllByIds(UsersById) returns (Users);
|
||||||
rpc Create(User) returns (UserById);
|
rpc Create(User) returns (UserById);
|
||||||
rpc Update(User) returns (UserById);
|
rpc Update(User) returns (UserById);
|
||||||
rpc Delete(UserById) returns (Empty);
|
rpc Delete(UserById) returns (Empty);
|
||||||
|
@ -14,6 +14,10 @@ message UserById {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UsersById {
|
||||||
|
repeated string ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message User {
|
message User {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string firstName = 2;
|
string firstName = 2;
|
||||||
|
@ -22,14 +26,8 @@ message User {
|
||||||
string phone = 5;
|
string phone = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UserFilter {
|
|
||||||
optional int32 page = 1;
|
|
||||||
optional int32 perPage = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Users {
|
message Users {
|
||||||
repeated User data = 1;
|
repeated User users = 1;
|
||||||
int32 total = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Empty {}
|
message Empty {}
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||||
|
import { FindUsersByIdsQueryHandler } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query-handler';
|
||||||
|
import { FindUsersByIdsQuery } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query';
|
||||||
|
|
||||||
|
const now = new Date('2023-06-21 06:00:00');
|
||||||
|
const user1: UserEntity = new UserEntity({
|
||||||
|
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
|
props: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
const user2: UserEntity = new UserEntity({
|
||||||
|
id: 'c160cf8c-f057-4962-841f-3ad68346df55',
|
||||||
|
props: {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'jane.doe@email.com',
|
||||||
|
phone: '+33611223355',
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
const user3: UserEntity = new UserEntity({
|
||||||
|
id: 'c160cf8c-f057-4962-841f-3ad68346df66',
|
||||||
|
props: {
|
||||||
|
firstName: 'Jimmy',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'jimmy.doe@email.com',
|
||||||
|
phone: '+33611223366',
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockUserRepository = {
|
||||||
|
findAllByIds: jest.fn().mockImplementation(() => [user1, user2, user3]),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Find Users By Ids Query Handler', () => {
|
||||||
|
let findUsersByIdsQueryHandler: FindUsersByIdsQueryHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: USER_REPOSITORY,
|
||||||
|
useValue: mockUserRepository,
|
||||||
|
},
|
||||||
|
FindUsersByIdsQueryHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
findUsersByIdsQueryHandler = module.get<FindUsersByIdsQueryHandler>(
|
||||||
|
FindUsersByIdsQueryHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(findUsersByIdsQueryHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution', () => {
|
||||||
|
it('should return users', async () => {
|
||||||
|
const findUsersByIdsQuery = new FindUsersByIdsQuery([
|
||||||
|
'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
|
'c160cf8c-f057-4962-841f-3ad68346df55',
|
||||||
|
'c160cf8c-f057-4962-841f-3ad68346df66',
|
||||||
|
]);
|
||||||
|
const users: UserEntity[] =
|
||||||
|
await findUsersByIdsQueryHandler.execute(findUsersByIdsQuery);
|
||||||
|
expect(users).toHaveLength(3);
|
||||||
|
expect(users[1].getProps().firstName).toBe('Jane');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { FindUsersByIdsGrpcController } from '@modules/user/interface/grpc-controllers/find-users-by-ids.grpc.controller';
|
||||||
|
import { UserMapper } from '@modules/user/user.mapper';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockQueryBus = {
|
||||||
|
execute: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => [
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
|
||||||
|
])
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUserMapper = {
|
||||||
|
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Find Users By Ids Grpc Controller', () => {
|
||||||
|
let findUsersbyIdsGrpcController: FindUsersByIdsGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: QueryBus,
|
||||||
|
useValue: mockQueryBus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserMapper,
|
||||||
|
useValue: mockUserMapper,
|
||||||
|
},
|
||||||
|
FindUsersByIdsGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
findUsersbyIdsGrpcController = module.get<FindUsersByIdsGrpcController>(
|
||||||
|
FindUsersByIdsGrpcController,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(findUsersbyIdsGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return users', async () => {
|
||||||
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
|
jest.spyOn(mockUserMapper, 'toResponse');
|
||||||
|
const response = await findUsersbyIdsGrpcController.findAllbyIds({
|
||||||
|
ids: [
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(response.users).toHaveLength(3);
|
||||||
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a generic RpcException', async () => {
|
||||||
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
|
jest.spyOn(mockUserMapper, 'toResponse');
|
||||||
|
expect.assertions(4);
|
||||||
|
try {
|
||||||
|
await findUsersbyIdsGrpcController.findAllbyIds({
|
||||||
|
ids: [
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
|
||||||
|
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||||
|
}
|
||||||
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -20,6 +20,8 @@ import { PublishMessageWhenUserIsCreatedDomainEventHandler } from './core/applic
|
||||||
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
|
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
|
||||||
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
|
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
|
||||||
import { RedisClientOptions } from '@songkeys/nestjs-redis';
|
import { RedisClientOptions } from '@songkeys/nestjs-redis';
|
||||||
|
import { FindUsersByIdsGrpcController } from './interface/grpc-controllers/find-users-by-ids.grpc.controller';
|
||||||
|
import { FindUsersByIdsQueryHandler } from './core/application/queries/find-users-by-ids/find-users-by-ids.query-handler';
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
|
@ -42,6 +44,7 @@ const grpcControllers = [
|
||||||
UpdateUserGrpcController,
|
UpdateUserGrpcController,
|
||||||
DeleteUserGrpcController,
|
DeleteUserGrpcController,
|
||||||
FindUserByIdGrpcController,
|
FindUserByIdGrpcController,
|
||||||
|
FindUsersByIdsGrpcController,
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventHandlers: Provider[] = [
|
const eventHandlers: Provider[] = [
|
||||||
|
@ -56,7 +59,10 @@ const commandHandlers: Provider[] = [
|
||||||
DeleteUserService,
|
DeleteUserService,
|
||||||
];
|
];
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [FindUserByIdQueryHandler];
|
const queryHandlers: Provider[] = [
|
||||||
|
FindUserByIdQueryHandler,
|
||||||
|
FindUsersByIdsQueryHandler,
|
||||||
|
];
|
||||||
|
|
||||||
const mappers: Provider[] = [UserMapper];
|
const mappers: Provider[] = [UserMapper];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { repl } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
await repl(AppModule);
|
||||||
|
}
|
||||||
|
bootstrap();
|
Loading…
Reference in New Issue