wip
This commit is contained in:
parent
ee9474828e
commit
d5c2bb396d
|
@ -7,9 +7,9 @@ HEALTH_SERVICE_PORT=6001
|
||||||
# PRISMA
|
# PRISMA
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
||||||
|
|
||||||
# RABBIT MQ
|
# MESSAGE BROKER
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||||
RMQ_EXCHANGE=mobicoop
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
REDIS_HOST=v3-redis
|
REDIS_HOST=v3-redis
|
||||||
|
|
|
@ -9,14 +9,13 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automapper/classes": "^8.7.7",
|
|
||||||
"@automapper/core": "^8.7.7",
|
|
||||||
"@automapper/nestjs": "^8.7.7",
|
|
||||||
"@grpc/grpc-js": "^1.8.0",
|
"@grpc/grpc-js": "^1.8.0",
|
||||||
"@grpc/proto-loader": "^0.7.4",
|
"@grpc/proto-loader": "^0.7.4",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.1.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
"@mobicoop/message-broker-module": "^1.0.5",
|
"@mobicoop/ddd-library": "^0.3.0",
|
||||||
|
"@mobicoop/health-module": "^2.0.0",
|
||||||
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/cache-manager": "^1.0.0",
|
"@nestjs/cache-manager": "^1.0.0",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
|
@ -32,6 +31,7 @@
|
||||||
"cache-manager": "^5.2.1",
|
"cache-manager": "^5.2.1",
|
||||||
"cache-manager-ioredis-yet": "^1.1.0",
|
"cache-manager-ioredis-yet": "^1.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
"ioredis": "^5.3.0"
|
"ioredis": "^5.3.0"
|
||||||
},
|
},
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"@types/jest": "^29.2.5",
|
"@types/jest": "^29.2.5",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
@ -174,39 +175,6 @@
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@automapper/classes": {
|
|
||||||
"version": "8.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@automapper/classes/-/classes-8.7.7.tgz",
|
|
||||||
"integrity": "sha512-FSbvt6QE8XnhKKQZA3kpKLuLrr9x1iW+lNYTrawVLjxQ05zsCGccLxe7moMNrg1wFAVAouQKupFgCGQ7XRjmJw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@automapper/core": "8.7.7",
|
|
||||||
"reflect-metadata": "~0.1.13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@automapper/core": {
|
|
||||||
"version": "8.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@automapper/core/-/core-8.7.7.tgz",
|
|
||||||
"integrity": "sha512-YfpDJ/xqwUuC0S+BLNk81ZJfeL7CmjirUX/Gk9eQyx146DKvneBZgeZ9v5rDB51Ti14jTxVHis+5JuT7W/q0TA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@automapper/nestjs": {
|
|
||||||
"version": "8.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@automapper/nestjs/-/nestjs-8.7.7.tgz",
|
|
||||||
"integrity": "sha512-9/uYY2cmN7SJjr2QxnfyXsteHrn/RHD+Dg0VMBflzK/e8Bh/KWyOve7+kaFixlUoyHe44aXs2LVaCslqt8wnhQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@automapper/core": "8.7.7",
|
|
||||||
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
|
||||||
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.21.4",
|
"version": "7.21.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
||||||
|
@ -259,9 +227,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core/node_modules/semver": {
|
"node_modules/@babel/core/node_modules/semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
@ -311,9 +279,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
@ -1938,12 +1906,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mobicoop/configuration-module": {
|
"node_modules/@mobicoop/configuration-module": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.2.0.tgz",
|
||||||
"integrity": "sha512-4yzCrY8m40XOO3CZnWJC4kHk66sTQCwe5UjKCV/UpNkN9IGUKW+R84J/53aulmGTL95vec7g6tFIwlHJd9BCoA==",
|
"integrity": "sha512-l0iDae7SgVVmjnCa2MBqAr3Er0yn4E7yiG8e7cs4XtNGUKrC1N0Ju56TEAraEYK9aZAZ36TCs06m1fep+rgwFA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/message-broker-module": "^1.0.4",
|
|
||||||
"@nestjs/cqrs": "^9.0.4",
|
"@nestjs/cqrs": "^9.0.4",
|
||||||
"@types/amqplib": "^0.10.1",
|
"@types/amqplib": "^0.10.1",
|
||||||
"amqplib": "^0.10.3",
|
"amqplib": "^0.10.3",
|
||||||
|
@ -1954,10 +1922,43 @@
|
||||||
"@nestjs/common": "^9.4.2"
|
"@nestjs/common": "^9.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mobicoop/ddd-library": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-MoUDqlrDmJkumCFSyW9FY2DLbguT4rytFrmBt9tVNCr2Es6nlz4Ml3HVBwJTZrlJFU79XmiUQ5WAO0MHJt+nAg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/event-emitter": "^1.4.2",
|
||||||
|
"@nestjs/microservices": "^9.4.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^9.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mobicoop/health-module": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mobicoop/health-module/-/health-module-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-r/7zrHJKVRTIiZ50ILy3lEUC/9vi6k0TRcYPMS8zcnUssQg+MPcT5DQS9B9tTB2gkKwcCyxOQlZZIppIybFX3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.8.14",
|
||||||
|
"@grpc/proto-loader": "^0.7.7",
|
||||||
|
"@mobicoop/ddd-library": "^0.3.0",
|
||||||
|
"@mobicoop/message-broker-module": "^1.0.5",
|
||||||
|
"@nestjs/axios": "^3.0.0",
|
||||||
|
"@nestjs/microservices": "^9.4.2",
|
||||||
|
"@nestjs/terminus": "^9.2.2",
|
||||||
|
"axios": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^9.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mobicoop/message-broker-module": {
|
"node_modules/@mobicoop/message-broker-module": {
|
||||||
"version": "1.0.5",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz",
|
||||||
"integrity": "sha512-9l2qCUXide2R1GzTmH1Z8CDHV0+zPJVp1OAk0q+PW9M73id6vX3j0uXURhGLQOwe01IhEMKkFs+D6YNPUPqqmw==",
|
"integrity": "sha512-RoSHHK1GyQ/QVDmm3JS/wBfh171oChvyEp6YWmJd12krFLrPVn9MoEvZdyT3I5J31oBiUabMPle5Kdpw+Nrmww==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
||||||
"@types/amqplib": "^0.10.1",
|
"@types/amqplib": "^0.10.1",
|
||||||
|
@ -1967,6 +1968,17 @@
|
||||||
"@nestjs/common": "^9.4.2"
|
"@nestjs/common": "^9.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/axios": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"axios": "^1.3.1",
|
||||||
|
"reflect-metadata": "^0.1.12",
|
||||||
|
"rxjs": "^6.0.0 || ^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cache-manager": {
|
"node_modules/@nestjs/cache-manager": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz",
|
||||||
|
@ -2193,6 +2205,19 @@
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/event-emitter": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-5mskPMS4KVH6LghC+NynfdmGiMCOOv9CdgVpuWGipLrJECv5KWc7vaW5o/9BYrcqPkN7Ted6CJ+O4AfsTiRlgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter2": "6.4.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||||
|
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||||
|
"reflect-metadata": "^0.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/microservices": {
|
"node_modules/@nestjs/microservices": {
|
||||||
"version": "9.4.2",
|
"version": "9.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.2.tgz",
|
||||||
|
@ -2754,6 +2779,12 @@
|
||||||
"@types/superagent": "*"
|
"@types/superagent": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/validator": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.7.17",
|
"version": "13.7.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz",
|
||||||
|
@ -3340,8 +3371,17 @@
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
"dev": true
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "28.1.3",
|
"version": "28.1.3",
|
||||||
|
@ -4043,7 +4083,6 @@
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -4284,7 +4323,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
|
@ -4764,6 +4802,11 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter2": {
|
||||||
|
"version": "6.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
|
||||||
|
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
|
||||||
|
},
|
||||||
"node_modules/events": {
|
"node_modules/events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
@ -5130,6 +5173,25 @@
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz",
|
||||||
|
@ -5162,7 +5224,6 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
@ -5228,20 +5289,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"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.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
@ -5776,9 +5823,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/istanbul-lib-instrument/node_modules/semver": {
|
"node_modules/istanbul-lib-instrument/node_modules/semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
@ -7362,9 +7409,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/make-dir/node_modules/semver": {
|
"node_modules/make-dir/node_modules/semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
@ -8133,9 +8180,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
|
||||||
"integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==",
|
"integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
@ -8172,6 +8219,11 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -8583,9 +8635,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.5.1",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
@ -9782,9 +9834,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
44
package.json
44
package.json
|
@ -17,28 +17,26 @@
|
||||||
"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": "npm run test:unit && npm run test:integration",
|
||||||
"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": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
|
"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",
|
||||||
"generate": "docker exec v3-user-api sh -c 'npx prisma generate'",
|
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
|
||||||
"migrate": "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": {
|
||||||
"@automapper/classes": "^8.7.7",
|
|
||||||
"@automapper/core": "^8.7.7",
|
|
||||||
"@automapper/nestjs": "^8.7.7",
|
|
||||||
"@grpc/grpc-js": "^1.8.0",
|
"@grpc/grpc-js": "^1.8.0",
|
||||||
"@grpc/proto-loader": "^0.7.4",
|
"@grpc/proto-loader": "^0.7.4",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.1.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
"@mobicoop/message-broker-module": "^1.0.5",
|
"@mobicoop/ddd-library": "^0.3.0",
|
||||||
|
"@mobicoop/health-module": "^2.0.0",
|
||||||
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/cache-manager": "^1.0.0",
|
"@nestjs/cache-manager": "^1.0.0",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
|
@ -54,6 +52,7 @@
|
||||||
"cache-manager": "^5.2.1",
|
"cache-manager": "^5.2.1",
|
||||||
"cache-manager-ioredis-yet": "^1.1.0",
|
"cache-manager-ioredis-yet": "^1.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
"ioredis": "^5.3.0"
|
"ioredis": "^5.3.0"
|
||||||
},
|
},
|
||||||
|
@ -63,6 +62,7 @@
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"@types/jest": "^29.2.5",
|
"@types/jest": "^29.2.5",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
@ -84,12 +84,12 @@
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
|
"prisma.service.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
@ -101,15 +101,19 @@
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
|
"prisma.service.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||||
|
"^@src(.*)": "<rootDir>$1"
|
||||||
|
},
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export const MESSAGE_BROKER_PUBLISHER = Symbol();
|
|
||||||
export const MESSAGE_PUBLISHER = Symbol();
|
|
|
@ -1,70 +1,65 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
|
||||||
import { UserModule } from './modules/user/user.module';
|
import { UserModule } from './modules/user/user.module';
|
||||||
import {
|
import {
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
ConfigurationModuleOptions,
|
ConfigurationModuleOptions,
|
||||||
} from '@mobicoop/configuration-module';
|
} from '@mobicoop/configuration-module';
|
||||||
import {
|
import {
|
||||||
MessageBrokerModule,
|
HealthModule,
|
||||||
MessageBrokerModuleOptions,
|
HealthModuleOptions,
|
||||||
} from '@mobicoop/message-broker-module';
|
HealthRepositoryPort,
|
||||||
|
} from '@mobicoop/health-module';
|
||||||
|
import { MessagerModule } from './modules/messager/messager.module';
|
||||||
|
import { USER_REPOSITORY } from './modules/user/user.di-tokens';
|
||||||
|
import { MESSAGE_PUBLISHER } from './modules/messager/messager.di-tokens';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
ConfigurationModule.forRootAsync({
|
||||||
UserModule,
|
imports: [ConfigModule],
|
||||||
MessageBrokerModule.forRootAsync(
|
inject: [ConfigService],
|
||||||
{
|
useFactory: async (
|
||||||
imports: [ConfigModule],
|
configService: ConfigService,
|
||||||
inject: [ConfigService],
|
): Promise<ConfigurationModuleOptions> => ({
|
||||||
useFactory: async (
|
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||||
configService: ConfigService,
|
messageBroker: {
|
||||||
): Promise<MessageBrokerModuleOptions> => ({
|
|
||||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||||
handlers: {},
|
},
|
||||||
}),
|
redis: {
|
||||||
},
|
host: configService.get<string>('REDIS_HOST'),
|
||||||
false,
|
password: configService.get<string>('REDIS_PASSWORD'),
|
||||||
),
|
port: configService.get<number>('REDIS_PORT'),
|
||||||
ConfigurationModule.forRootAsync(
|
},
|
||||||
{
|
setConfigurationBrokerQueue: 'user-configuration-create-update',
|
||||||
imports: [ConfigModule],
|
deleteConfigurationQueue: 'user-configuration-delete',
|
||||||
inject: [ConfigService],
|
propagateConfigurationQueue: 'user-configuration-propagate',
|
||||||
useFactory: async (
|
}),
|
||||||
configService: ConfigService,
|
}),
|
||||||
): Promise<ConfigurationModuleOptions> => ({
|
HealthModule.forRootAsync({
|
||||||
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
imports: [UserModule, MessagerModule],
|
||||||
messageBroker: {
|
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],
|
||||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
useFactory: async (
|
||||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
userRepository: HealthRepositoryPort,
|
||||||
|
messagePublisher: MessagePublisherPort,
|
||||||
|
): Promise<HealthModuleOptions> => ({
|
||||||
|
serviceName: 'user',
|
||||||
|
criticalLoggingKey: 'logging.user.health.crit',
|
||||||
|
checkRepositories: [
|
||||||
|
{
|
||||||
|
name: 'UserRepository',
|
||||||
|
repository: userRepository,
|
||||||
},
|
},
|
||||||
redis: {
|
],
|
||||||
host: configService.get<string>('REDIS_HOST'),
|
messagePublisher,
|
||||||
password: configService.get<string>('REDIS_PASSWORD'),
|
}),
|
||||||
port: configService.get<number>('REDIS_PORT'),
|
}),
|
||||||
},
|
UserModule,
|
||||||
setConfigurationBrokerRoutingKeys: [
|
MessagerModule,
|
||||||
'configuration.create',
|
|
||||||
'configuration.update',
|
|
||||||
],
|
|
||||||
deleteConfigurationRoutingKey: 'configuration.delete',
|
|
||||||
propagateConfigurationRoutingKey: 'configuration.propagate',
|
|
||||||
setConfigurationBrokerQueue: 'user-configuration-create-update',
|
|
||||||
deleteConfigurationQueue: 'user-configuration-delete',
|
|
||||||
propagateConfigurationQueue: 'user-configuration-propagate',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
HealthModule,
|
|
||||||
],
|
],
|
||||||
controllers: [],
|
exports: [UserModule, MessagerModule],
|
||||||
providers: [],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
||||||
|
|
||||||
package health;
|
package health;
|
||||||
|
|
||||||
|
|
||||||
service Health {
|
service Health {
|
||||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
}
|
}
|
||||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
||||||
NOT_SERVING = 2;
|
NOT_SERVING = 2;
|
||||||
}
|
}
|
||||||
ServingStatus status = 1;
|
ServingStatus status = 1;
|
||||||
|
string message = 2;
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export interface IPublishMessage {
|
|
||||||
publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -13,8 +13,8 @@ async function bootstrap() {
|
||||||
options: {
|
options: {
|
||||||
package: ['user', 'health'],
|
package: ['user', 'health'],
|
||||||
protoPath: [
|
protoPath: [
|
||||||
join(__dirname, 'modules/user/adapters/primaries/user.proto'),
|
join(__dirname, 'modules/user/interface/grpc-controllers/user.proto'),
|
||||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
join(__dirname, 'health.proto'),
|
||||||
],
|
],
|
||||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||||
loader: { keepCase: true },
|
loader: { keepCase: true },
|
||||||
|
|
|
@ -1,259 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { ICollection } from '../../interfaces/collection.interface';
|
|
||||||
import { IRepository } from '../../interfaces/repository.interface';
|
|
||||||
import { PrismaService } from './prisma-service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Child classes MUST redefined _model property with appropriate model name
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export abstract class PrismaRepository<T> implements IRepository<T> {
|
|
||||||
protected model: string;
|
|
||||||
|
|
||||||
constructor(protected readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
findAll = async (
|
|
||||||
page = 1,
|
|
||||||
perPage = 10,
|
|
||||||
where?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const [data, total] = await this.prisma.$transaction([
|
|
||||||
this.prisma[this.model].findMany({
|
|
||||||
where,
|
|
||||||
include,
|
|
||||||
skip: (page - 1) * perPage,
|
|
||||||
take: perPage,
|
|
||||||
}),
|
|
||||||
this.prisma[this.model].count({
|
|
||||||
where,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
findOneByUuid = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findUnique({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findOne = async (where: any, include?: any): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findFirst({
|
|
||||||
where: where,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO : using any is not good, but needed for nested entities
|
|
||||||
// TODO : Refactor for good clean architecture ?
|
|
||||||
create = async (entity: Partial<T> | any, include?: any): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const res = await this.prisma[this.model].create({
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
update = async (uuid: string, entity: Partial<T>): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: { uuid },
|
|
||||||
data: entity,
|
|
||||||
});
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWhere = async (
|
|
||||||
where: any,
|
|
||||||
entity: Partial<T> | any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: where,
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
delete = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].delete({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteMany = async (where: any): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].deleteMany({
|
|
||||||
where: where,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findAllByQuery = async (
|
|
||||||
include: string[],
|
|
||||||
where: string[],
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const query = `SELECT ${include.join(',')} FROM ${
|
|
||||||
this.model
|
|
||||||
} WHERE ${where.join(' AND ')}`;
|
|
||||||
const data: T[] = await this.prisma.$queryRawUnsafe(query);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total: data.length,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createWithFields = async (fields: object): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
|
|
||||||
'","',
|
|
||||||
)}") VALUES (${Object.values(fields).join(',')})`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWithFields = async (uuid: string, entity: object): Promise<number> => {
|
|
||||||
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
|
|
||||||
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
|
|
||||||
try {
|
|
||||||
const command = `UPDATE ${this.model} SET ${values.join(
|
|
||||||
', ',
|
|
||||||
)} WHERE uuid = '${uuid}'`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
healthCheck = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await this.prisma.$queryRaw`SELECT 1`;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PrismaService } from './adapters/secondaries/prisma-service';
|
|
||||||
import { UserRepository } from './domain/user-repository';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [PrismaService, UserRepository],
|
|
||||||
exports: [PrismaService, UserRepository],
|
|
||||||
})
|
|
||||||
export class DatabaseModule {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
|
||||||
|
|
||||||
export class UserRepository<T> extends PrismaRepository<T> {}
|
|
|
@ -1,24 +0,0 @@
|
||||||
export class DatabaseException implements Error {
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _type: string = 'unknown',
|
|
||||||
private _code: string = '',
|
|
||||||
message?: string,
|
|
||||||
) {
|
|
||||||
this.name = 'DatabaseException';
|
|
||||||
this.message = message ?? 'An error occured with the database.';
|
|
||||||
if (this.message.includes('Unique constraint failed')) {
|
|
||||||
this.message = 'Already exists.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get type(): string {
|
|
||||||
return this._type;
|
|
||||||
}
|
|
||||||
|
|
||||||
get code(): string {
|
|
||||||
return this._code;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface ICollection<T> {
|
|
||||||
data: T[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { ICollection } from './collection.interface';
|
|
||||||
|
|
||||||
export interface IRepository<T> {
|
|
||||||
findAll(
|
|
||||||
page: number,
|
|
||||||
perPage: number,
|
|
||||||
params?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>>;
|
|
||||||
findOne(where: any, include?: any): Promise<T>;
|
|
||||||
findOneByUuid(uuid: string, include?: any): Promise<T>;
|
|
||||||
create(entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
|
|
||||||
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
delete(uuid: string): Promise<T>;
|
|
||||||
deleteMany(where: any): Promise<void>;
|
|
||||||
healthCheck(): Promise<boolean>;
|
|
||||||
}
|
|
|
@ -1,571 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PrismaService } from '../../adapters/secondaries/prisma-service';
|
|
||||||
import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
class FakeEntity {
|
|
||||||
uuid?: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entityId = 2;
|
|
||||||
const entityUuid = 'uuid-';
|
|
||||||
const entityName = 'name-';
|
|
||||||
|
|
||||||
const createRandomEntity = (): FakeEntity => {
|
|
||||||
const entity: FakeEntity = {
|
|
||||||
uuid: `${entityUuid}${entityId}`,
|
|
||||||
name: `${entityName}${entityId}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
entityId++;
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityToCreate: FakeEntity = {
|
|
||||||
name: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityCreated: FakeEntity = {
|
|
||||||
...fakeEntityToCreate,
|
|
||||||
uuid: 'some-uuid',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntities: FakeEntity[] = [];
|
|
||||||
Array.from({ length: 10 }).forEach(() => {
|
|
||||||
fakeEntities.push(createRandomEntity());
|
|
||||||
});
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
|
|
||||||
protected model = 'fake';
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakePrismaService extends PrismaService {
|
|
||||||
fake: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
|
||||||
$transaction: jest.fn().mockImplementation(async (data: any) => {
|
|
||||||
const entities = await data[0];
|
|
||||||
if (entities.length == 1) {
|
|
||||||
return Promise.resolve([[fakeEntityCreated], 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve([fakeEntities, fakeEntities.length]);
|
|
||||||
}),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
$queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
$executeRawUnsafe: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
$queryRaw: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
fake: {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
findMany: jest.fn().mockImplementation((params?: any) => {
|
|
||||||
if (params?.where?.limit == 1) {
|
|
||||||
return Promise.resolve([fakeEntityCreated]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
count: jest.fn().mockResolvedValue(fakeEntities.length),
|
|
||||||
|
|
||||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
|
||||||
let entity;
|
|
||||||
|
|
||||||
if (params?.where?.uuid) {
|
|
||||||
entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params?.where?.uuid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entity && params?.where?.uuid == 'unknown') {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
} else if (!entity) {
|
|
||||||
throw new Error('no entity');
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}),
|
|
||||||
|
|
||||||
findFirst: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
if (params?.where?.name) {
|
|
||||||
return Promise.resolve(
|
|
||||||
fakeEntities.find((entity) => entity.name === params?.where?.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.name === params.where.name,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params.where.uuid,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteMany: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('PrismaRepository', () => {
|
|
||||||
let fakeRepository: FakePrismaRepository;
|
|
||||||
let prisma: FakePrismaService;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
FakePrismaRepository,
|
|
||||||
{
|
|
||||||
provide: PrismaService,
|
|
||||||
useValue: mockPrismaService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
|
|
||||||
prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(fakeRepository).toBeDefined();
|
|
||||||
expect(prisma).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'findMany');
|
|
||||||
jest.spyOn(prisma.fake, 'count');
|
|
||||||
jest.spyOn(prisma, '$transaction');
|
|
||||||
|
|
||||||
const entities = await fakeRepository.findAll();
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an array containing only one entity', async () => {
|
|
||||||
const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
|
|
||||||
|
|
||||||
expect(prisma.fake.findMany).toHaveBeenCalledWith({
|
|
||||||
skip: 0,
|
|
||||||
take: 10,
|
|
||||||
where: { limit: 1 },
|
|
||||||
});
|
|
||||||
expect(entities).toEqual({
|
|
||||||
data: [fakeEntityCreated],
|
|
||||||
total: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'create');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.create(fakeEntityToCreate);
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should find an entity by uuid', async () => {
|
|
||||||
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
|
|
||||||
expect(entity).toBe(fakeEntities[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('unknown'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('wrong-uuid'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOne', () => {
|
|
||||||
it('should find one entity', async () => {
|
|
||||||
const entity = await fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(entity.name).toBe(fakeEntities[0].name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for unknown error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with name', async () => {
|
|
||||||
const newName = 'new-random-name';
|
|
||||||
|
|
||||||
await fakeRepository.updateWhere(
|
|
||||||
{ name: fakeEntities[0].name },
|
|
||||||
{
|
|
||||||
name: newName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with uuid', async () => {
|
|
||||||
const newName = 'random-name';
|
|
||||||
|
|
||||||
await fakeRepository.update(fakeEntities[0].uuid, {
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete an entity', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.delete(savedUuid);
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteMany', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete entities based on their uuid', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.deleteMany({ uuid: savedUuid });
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAllByquery', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
const entities = await fakeRepository.findAllByQuery(
|
|
||||||
['uuid', 'name'],
|
|
||||||
['name is not null'],
|
|
||||||
);
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createWithFields', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
});
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateWithFields', () => {
|
|
||||||
it('should update an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const updatedEntity = await fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(updatedEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('healthCheck', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a healthy result', async () => {
|
|
||||||
const res = await fakeRepository.healthCheck();
|
|
||||||
expect(res).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if database is not available', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { GrpcMethod } from '@nestjs/microservices';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
|
|
||||||
enum ServingStatus {
|
|
||||||
UNKNOWN = 0,
|
|
||||||
SERVING = 1,
|
|
||||||
NOT_SERVING = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HealthCheckRequest {
|
|
||||||
service: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HealthCheckResponse {
|
|
||||||
status: ServingStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class HealthServerController {
|
|
||||||
constructor(
|
|
||||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GrpcMethod('Health', 'Check')
|
|
||||||
async check(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
data: HealthCheckRequest,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
metadata: any,
|
|
||||||
): Promise<HealthCheckResponse> {
|
|
||||||
const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy(
|
|
||||||
'prisma',
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status:
|
|
||||||
healthCheck['prisma'].status == 'up'
|
|
||||||
? ServingStatus.SERVING
|
|
||||||
: ServingStatus.NOT_SERVING,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { Controller, Get, Inject } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthCheckService,
|
|
||||||
HealthCheck,
|
|
||||||
HealthCheckResult,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
import { MESSAGE_PUBLISHER } from 'src/app.constants';
|
|
||||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
|
|
||||||
|
|
||||||
@Controller('health')
|
|
||||||
export class HealthController {
|
|
||||||
constructor(
|
|
||||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
|
||||||
private readonly healthCheckService: HealthCheckService,
|
|
||||||
@Inject(MESSAGE_PUBLISHER)
|
|
||||||
private readonly messagePublisher: IPublishMessage,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@HealthCheck()
|
|
||||||
async check() {
|
|
||||||
try {
|
|
||||||
return await this.healthCheckService.check([
|
|
||||||
async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
const healthCheckResult: HealthCheckResult = error.response;
|
|
||||||
this.messagePublisher.publish(
|
|
||||||
'logging.user.health.crit',
|
|
||||||
JSON.stringify(healthCheckResult.error),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
|
||||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MessagePublisher implements IPublishMessage {
|
|
||||||
constructor(
|
|
||||||
@Inject(MESSAGE_BROKER_PUBLISHER)
|
|
||||||
private readonly messageBrokerPublisher: MessageBrokerPublisher,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
publish = (routingKey: string, message: string): void => {
|
|
||||||
this.messageBrokerPublisher.publish(routingKey, message);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthCheckError,
|
|
||||||
HealthIndicator,
|
|
||||||
HealthIndicatorResult,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { UsersRepository } from '../../../user/adapters/secondaries/users.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
|
|
||||||
constructor(private readonly repository: UsersRepository) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
|
|
||||||
try {
|
|
||||||
await this.repository.healthCheck();
|
|
||||||
return this.getStatus(key, true);
|
|
||||||
} catch (e) {
|
|
||||||
throw new HealthCheckError('Prisma', {
|
|
||||||
prisma: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { HealthServerController } from './adapters/primaries/health-server.controller';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
import { UsersRepository } from '../user/adapters/secondaries/users.repository';
|
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
import { HealthController } from './adapters/primaries/health.controller';
|
|
||||||
import { TerminusModule } from '@nestjs/terminus';
|
|
||||||
import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants';
|
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
|
||||||
import { MessagePublisher } from './adapters/secondaries/message-publisher';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TerminusModule, DatabaseModule],
|
|
||||||
controllers: [HealthServerController, HealthController],
|
|
||||||
providers: [
|
|
||||||
PrismaHealthIndicatorUseCase,
|
|
||||||
UsersRepository,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_BROKER_PUBLISHER,
|
|
||||||
useClass: MessageBrokerPublisher,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: MESSAGE_PUBLISHER,
|
|
||||||
useClass: MessagePublisher,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
|
|
||||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
const mockMessageBrokerPublisher = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Messager', () => {
|
|
||||||
let messagePublisher: MessagePublisher;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
MessagePublisher,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_BROKER_PUBLISHER,
|
|
||||||
useValue: mockMessageBrokerPublisher,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(messagePublisher).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish a message', async () => {
|
|
||||||
jest.spyOn(mockMessageBrokerPublisher, 'publish');
|
|
||||||
messagePublisher.publish('health.info', 'my-test');
|
|
||||||
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
import { UsersRepository } from '../../../user/adapters/secondaries/users.repository';
|
|
||||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
const mockUsersRepository = {
|
|
||||||
healthCheck: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(true);
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Service unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('PrismaHealthIndicatorUseCase', () => {
|
|
||||||
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: UsersRepository,
|
|
||||||
useValue: mockUsersRepository,
|
|
||||||
},
|
|
||||||
PrismaHealthIndicatorUseCase,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
|
|
||||||
PrismaHealthIndicatorUseCase,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(prismaHealthIndicatorUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should check health successfully', async () => {
|
|
||||||
const healthIndicatorResult: HealthIndicatorResult =
|
|
||||||
await prismaHealthIndicatorUseCase.isHealthy('prisma');
|
|
||||||
|
|
||||||
expect(healthIndicatorResult['prisma'].status).toBe('up');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if database is unavailable', async () => {
|
|
||||||
await expect(
|
|
||||||
prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
|
||||||
).rejects.toBeInstanceOf(HealthCheckError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Module, Provider } from '@nestjs/common';
|
||||||
|
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||||
|
import {
|
||||||
|
MessageBrokerModule,
|
||||||
|
MessageBrokerModuleOptions,
|
||||||
|
MessageBrokerPublisher,
|
||||||
|
} from '@mobicoop/message-broker-module';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
const imports = [
|
||||||
|
MessageBrokerModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (
|
||||||
|
configService: ConfigService,
|
||||||
|
): Promise<MessageBrokerModuleOptions> => ({
|
||||||
|
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||||
|
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||||
|
name: 'user',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const providers: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: MESSAGE_PUBLISHER,
|
||||||
|
useClass: MessageBrokerPublisher,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports,
|
||||||
|
providers,
|
||||||
|
exports: [MESSAGE_PUBLISHER],
|
||||||
|
})
|
||||||
|
export class MessagerModule {}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class CreateUserCommand extends Command {
|
||||||
|
readonly firstName?: string;
|
||||||
|
readonly lastName?: string;
|
||||||
|
readonly email?: string;
|
||||||
|
readonly phone?: string;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<CreateUserCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.firstName = props.firstName;
|
||||||
|
this.lastName = props.lastName;
|
||||||
|
this.email = props.email;
|
||||||
|
this.phone = props.phone;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
|
import { CreateUserCommand } from './create-user.command';
|
||||||
|
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||||
|
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||||
|
import { UserEntity } from '../../../domain/user.entity';
|
||||||
|
import { UserAlreadyExistsException } from '../../../domain/user.errors';
|
||||||
|
|
||||||
|
@CommandHandler(CreateUserCommand)
|
||||||
|
export class CreateUserService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(USER_REPOSITORY)
|
||||||
|
private readonly repository: UserRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: CreateUserCommand): Promise<AggregateID> {
|
||||||
|
const user = UserEntity.create({
|
||||||
|
firstName: command.firstName,
|
||||||
|
lastName: command.lastName,
|
||||||
|
email: command.email,
|
||||||
|
phone: command.phone,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.repository.insert(user);
|
||||||
|
return user.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new UserAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UpdateUserCommand extends Command {
|
||||||
|
readonly firstName?: string;
|
||||||
|
readonly lastName?: string;
|
||||||
|
readonly email?: string;
|
||||||
|
readonly phone?: string;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<UpdateUserCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.firstName = props.firstName;
|
||||||
|
this.lastName = props.lastName;
|
||||||
|
this.email = props.email;
|
||||||
|
this.phone = props.phone;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
ConflictException,
|
||||||
|
UniqueConstraintException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||||
|
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||||
|
import { UserEntity } from '../../../domain/user.entity';
|
||||||
|
import { UpdateUserCommand } from './update-user.command';
|
||||||
|
import {
|
||||||
|
EmailAlreadyExistsException,
|
||||||
|
PhoneAlreadyExistsException,
|
||||||
|
UserAlreadyExistsException,
|
||||||
|
} from '@modules/user/core/domain/user.errors';
|
||||||
|
|
||||||
|
@CommandHandler(UpdateUserCommand)
|
||||||
|
export class UpdateUserService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(USER_REPOSITORY)
|
||||||
|
private readonly userRepository: UserRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: UpdateUserCommand): Promise<AggregateID> {
|
||||||
|
try {
|
||||||
|
const user: UserEntity = await this.userRepository.findOneById(
|
||||||
|
command.id,
|
||||||
|
);
|
||||||
|
user.update({
|
||||||
|
firstName: command.firstName,
|
||||||
|
lastName: command.lastName,
|
||||||
|
email: command.email,
|
||||||
|
phone: command.phone,
|
||||||
|
});
|
||||||
|
await this.userRepository.update(user.id, user);
|
||||||
|
return user.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new UserAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error instanceof UniqueConstraintException &&
|
||||||
|
error.message.includes('email')
|
||||||
|
) {
|
||||||
|
throw new EmailAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error instanceof UniqueConstraintException &&
|
||||||
|
error.message.includes('phone')
|
||||||
|
) {
|
||||||
|
throw new PhoneAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||||
|
import { UserEntity } from '../../domain/user.entity';
|
||||||
|
|
||||||
|
export type UserRepositoryPort = RepositoryPort<UserEntity>;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UserCreatedDomainEvent extends DomainEvent {
|
||||||
|
readonly firstName: string;
|
||||||
|
readonly lastName: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly phone: string;
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<UserCreatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.firstName = props.firstName;
|
||||||
|
this.lastName = props.lastName;
|
||||||
|
this.email = props.email;
|
||||||
|
this.phone = props.phone;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UserUpdatedDomainEvent extends DomainEvent {
|
||||||
|
readonly firstName: string;
|
||||||
|
readonly lastName: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly phone: string;
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<UserUpdatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.firstName = props.firstName;
|
||||||
|
this.lastName = props.lastName;
|
||||||
|
this.email = props.email;
|
||||||
|
this.phone = props.phone;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { CreateUserProps, UpdateUserProps, UserProps } from './user.types';
|
||||||
|
import { UserCreatedDomainEvent } from './events/user-created.domain-events';
|
||||||
|
import { UserUpdatedDomainEvent } from './events/user-updated.domain-events';
|
||||||
|
|
||||||
|
export class UserEntity extends AggregateRoot<UserProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateUserProps): UserEntity => {
|
||||||
|
const id = v4();
|
||||||
|
const props: UserProps = { ...create };
|
||||||
|
const user = new UserEntity({ id, props });
|
||||||
|
|
||||||
|
user.addEvent(
|
||||||
|
new UserCreatedDomainEvent({
|
||||||
|
aggregateId: id,
|
||||||
|
firstName: props.firstName,
|
||||||
|
lastName: props.lastName,
|
||||||
|
email: props.email,
|
||||||
|
phone: props.phone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
update(props: UpdateUserProps): void {
|
||||||
|
this.props.firstName = props.firstName;
|
||||||
|
this.props.lastName = props.lastName;
|
||||||
|
this.props.email = props.email;
|
||||||
|
this.props.phone = props.phone;
|
||||||
|
this.addEvent(
|
||||||
|
new UserUpdatedDomainEvent({
|
||||||
|
aggregateId: this._id,
|
||||||
|
firstName: props.firstName,
|
||||||
|
lastName: props.lastName,
|
||||||
|
email: props.email,
|
||||||
|
phone: props.phone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UserAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'User already exists';
|
||||||
|
|
||||||
|
public readonly code = 'USER.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(UserAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Email already exists';
|
||||||
|
|
||||||
|
public readonly code = 'EMAIL.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(EmailAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PhoneAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Phone already exists';
|
||||||
|
|
||||||
|
public readonly code = 'PHONE.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(PhoneAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// All properties that a User has
|
||||||
|
export interface UserProps {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for a User creation
|
||||||
|
export interface CreateUserProps {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserProps {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
LoggerBase,
|
||||||
|
MessagePublisherPort,
|
||||||
|
PrismaRepositoryBase,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
import { UserEntity } from '../core/domain/user.entity';
|
||||||
|
import { UserRepositoryPort } from '../core/application/ports/user.repository.port';
|
||||||
|
import { UserMapper } from '../user.mapper';
|
||||||
|
import { USER_MESSAGE_PUBLISHER } from '../user.di-tokens';
|
||||||
|
|
||||||
|
export type UserModel = {
|
||||||
|
uuid: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository is used for retrieving/saving domain entities
|
||||||
|
* */
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository
|
||||||
|
extends PrismaRepositoryBase<UserEntity, UserModel, UserModel>
|
||||||
|
implements UserRepositoryPort
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
prisma: PrismaService,
|
||||||
|
mapper: UserMapper,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
@Inject(USER_MESSAGE_PUBLISHER)
|
||||||
|
protected readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
prisma.user,
|
||||||
|
prisma,
|
||||||
|
mapper,
|
||||||
|
eventEmitter,
|
||||||
|
new LoggerBase({
|
||||||
|
logger: new Logger(UserRepository.name),
|
||||||
|
domain: 'user',
|
||||||
|
messagePublisher,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { IdResponse } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
|
import { CreateUserRequestDto } from './dtos/create-user.request.dto';
|
||||||
|
import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command';
|
||||||
|
import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: false,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class CreateUserGrpcController {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('UserService', 'Create')
|
||||||
|
async create(data: CreateUserRequestDto): Promise<IdResponse> {
|
||||||
|
try {
|
||||||
|
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||||
|
new CreateUserCommand(data),
|
||||||
|
);
|
||||||
|
return new IdResponse(aggregateID);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof UserAlreadyExistsException)
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@IsOptional()
|
||||||
|
phone?: string;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateUserRequestDto {
|
||||||
|
@IsString()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@IsOptional()
|
||||||
|
phone?: string;
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
IdResponse,
|
||||||
|
RpcExceptionCode,
|
||||||
|
RpcValidationPipe,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { UpdateUserRequestDto } from './dtos/update-user.request.dto';
|
||||||
|
import { UpdateUserCommand } from '@modules/user/core/application/commands/update-user/update-user.command';
|
||||||
|
import {
|
||||||
|
EmailAlreadyExistsException,
|
||||||
|
PhoneAlreadyExistsException,
|
||||||
|
UserAlreadyExistsException,
|
||||||
|
} from '@modules/user/core/domain/user.errors';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class UpdateUserGrpcController {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('UserService', 'UpdateUser')
|
||||||
|
async updateUser(data: UpdateUserRequestDto): Promise<IdResponse> {
|
||||||
|
try {
|
||||||
|
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||||
|
new UpdateUserCommand({
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
email: data.email,
|
||||||
|
phone: data.phone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return new IdResponse(aggregateID);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error instanceof UserAlreadyExistsException ||
|
||||||
|
error instanceof EmailAlreadyExistsException ||
|
||||||
|
error instanceof PhoneAlreadyExistsException
|
||||||
|
)
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package user;
|
||||||
|
|
||||||
|
service UserService {
|
||||||
|
rpc FindOneByUuid(UserByUuid) returns (User);
|
||||||
|
rpc FindAll(UserFilter) returns (Users);
|
||||||
|
rpc Create(User) returns (User);
|
||||||
|
rpc Update(User) returns (User);
|
||||||
|
rpc Delete(UserByUuid) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserByUuid {
|
||||||
|
string uuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message User {
|
||||||
|
string uuid = 1;
|
||||||
|
string firstName = 2;
|
||||||
|
string lastName = 3;
|
||||||
|
string email = 4;
|
||||||
|
string phone = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserFilter {
|
||||||
|
optional int32 page = 1;
|
||||||
|
optional int32 perPage = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Users {
|
||||||
|
repeated User data = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Empty {}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||||
|
import { UserResponseDto } from './user.response.dto';
|
||||||
|
|
||||||
|
export class UserPaginatedResponseDto extends PaginatedResponseDto<UserResponseDto> {
|
||||||
|
readonly data: readonly UserResponseDto[];
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UserResponseDto extends ResponseBase {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { CreateUserProps } from '@modules/user/core/domain/user.types';
|
||||||
|
import { PrismaService } from '@modules/user/infrastructure/prisma.service';
|
||||||
|
import { UserRepository } from '@modules/user/infrastructure/user.repository';
|
||||||
|
import {
|
||||||
|
USER_MESSAGE_PUBLISHER,
|
||||||
|
USER_REPOSITORY,
|
||||||
|
} from '@modules/user/user.di-tokens';
|
||||||
|
import { UserMapper } from '@modules/user/user.mapper';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
describe('User Repository', () => {
|
||||||
|
let prismaService: PrismaService;
|
||||||
|
let userRepository: UserRepository;
|
||||||
|
|
||||||
|
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 createUsers = async (nbToCreate = 10) => {
|
||||||
|
for (let i = 0; i < nbToCreate; i++) {
|
||||||
|
const userToCreate = {
|
||||||
|
uuid: getSeed(i, baseUuid.uuid),
|
||||||
|
firstName: `John${i}`,
|
||||||
|
lastName: `Doe${i}`,
|
||||||
|
email: `john.doe${i}@email.com`,
|
||||||
|
phone: `+33611223344${i}`,
|
||||||
|
};
|
||||||
|
userToCreate.uuid = getSeed(i, baseUuid.uuid);
|
||||||
|
await executeInsertCommand('user', userToCreate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
UserMapper,
|
||||||
|
{
|
||||||
|
provide: USER_REPOSITORY,
|
||||||
|
useClass: UserRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: USER_MESSAGE_PUBLISHER,
|
||||||
|
useValue: mockMessagePublisher,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
// disable logging
|
||||||
|
.setLogger(mockLogger)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
|
userRepository = module.get<UserRepository>(USER_REPOSITORY);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prismaService.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prismaService.user.deleteMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findOneById', () => {
|
||||||
|
it('should return a user', async () => {
|
||||||
|
await createUsers(1);
|
||||||
|
const result = await userRepository.findOneById(baseUuid.uuid);
|
||||||
|
expect(result.id).toBe(baseUuid.uuid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should create a user', async () => {
|
||||||
|
const beforeCount = await prismaService.user.count();
|
||||||
|
|
||||||
|
const createUserProps: CreateUserProps = {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'jane.doe@email.com',
|
||||||
|
phone: '+33622334455',
|
||||||
|
};
|
||||||
|
|
||||||
|
const userToCreate: UserEntity = UserEntity.create(createUserProps);
|
||||||
|
await userRepository.insert(userToCreate);
|
||||||
|
|
||||||
|
const afterCount = await prismaService.user.count();
|
||||||
|
|
||||||
|
expect(afterCount - beforeCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,174 +0,0 @@
|
||||||
import { TestingModule, Test } from '@nestjs/testing';
|
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
|
||||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
|
||||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
|
||||||
import { User } from '../../domain/entities/user';
|
|
||||||
|
|
||||||
describe('UsersRepository', () => {
|
|
||||||
let prismaService: PrismaService;
|
|
||||||
let usersRepository: UsersRepository;
|
|
||||||
|
|
||||||
const createUsers = async (nbToCreate = 10) => {
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
await prismaService.user.create({
|
|
||||||
data: {
|
|
||||||
firstName: `firstName-${i}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [DatabaseModule],
|
|
||||||
providers: [UsersRepository, PrismaService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
|
||||||
usersRepository = module.get<UsersRepository>(UsersRepository);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await prismaService.$disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await prismaService.user.deleteMany();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an empty data array', async () => {
|
|
||||||
const res = await usersRepository.findAll();
|
|
||||||
expect(res).toEqual({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 8 users', async () => {
|
|
||||||
await createUsers(8);
|
|
||||||
const users = await usersRepository.findAll();
|
|
||||||
expect(users.data.length).toBe(8);
|
|
||||||
expect(users.total).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 users', async () => {
|
|
||||||
await createUsers(20);
|
|
||||||
const users = await usersRepository.findAll();
|
|
||||||
expect(users.data.length).toBe(10);
|
|
||||||
expect(users.total).toBe(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should return a user', async () => {
|
|
||||||
const userToFind = await prismaService.user.create({
|
|
||||||
data: {
|
|
||||||
firstName: 'test',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await usersRepository.findOneByUuid(userToFind.uuid);
|
|
||||||
expect(user.uuid).toBe(userToFind.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
|
||||||
const user = await usersRepository.findOneByUuid(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
);
|
|
||||||
expect(user).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOne', () => {
|
|
||||||
it('should return a user according to its email', async () => {
|
|
||||||
const userToFind = await prismaService.user.create({
|
|
||||||
data: {
|
|
||||||
email: 'test@test.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await usersRepository.findOne({
|
|
||||||
email: 'test@test.com',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.uuid).toBe(userToFind.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null with unknown email', async () => {
|
|
||||||
const user = await usersRepository.findOne({
|
|
||||||
email: 'wrong@email.com',
|
|
||||||
});
|
|
||||||
expect(user).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create a user', async () => {
|
|
||||||
const beforeCount = await prismaService.user.count();
|
|
||||||
|
|
||||||
const userToCreate: User = new User();
|
|
||||||
userToCreate.firstName = 'test';
|
|
||||||
const user = await usersRepository.create(userToCreate);
|
|
||||||
|
|
||||||
const afterCount = await prismaService.user.count();
|
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
|
||||||
expect(user.uuid).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should update user firstName', async () => {
|
|
||||||
const userToUpdate = await prismaService.user.create({
|
|
||||||
data: {
|
|
||||||
firstName: 'test',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toUpdate: User = new User();
|
|
||||||
toUpdate.firstName = 'updated';
|
|
||||||
const updateduser = await usersRepository.update(
|
|
||||||
userToUpdate.uuid,
|
|
||||||
toUpdate,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updateduser.uuid).toBe(userToUpdate.uuid);
|
|
||||||
expect(updateduser.firstName).toBe('updated');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
|
||||||
const toUpdate: User = new User();
|
|
||||||
toUpdate.firstName = 'updated';
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usersRepository.update(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
toUpdate,
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete a user', async () => {
|
|
||||||
const userToRemove = await prismaService.user.create({
|
|
||||||
data: {
|
|
||||||
firstName: 'test',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await usersRepository.delete(userToRemove.uuid);
|
|
||||||
|
|
||||||
const count = await prismaService.user.count();
|
|
||||||
expect(count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
|
||||||
await expect(
|
|
||||||
usersRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { ConflictException } from '@mobicoop/ddd-library';
|
||||||
|
import { CreateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto';
|
||||||
|
import { CreateUserService } from '@modules/user/core/application/commands/create-user/create-user.service';
|
||||||
|
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command';
|
||||||
|
import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors';
|
||||||
|
|
||||||
|
const createUserRequest: CreateUserRequestDto = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUserRepository = {
|
||||||
|
insert: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => ({}))
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new ConflictException('already exists');
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('create-user.service', () => {
|
||||||
|
let createUserService: CreateUserService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: USER_REPOSITORY,
|
||||||
|
useValue: mockUserRepository,
|
||||||
|
},
|
||||||
|
CreateUserService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
createUserService = module.get<CreateUserService>(CreateUserService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(createUserService).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution', () => {
|
||||||
|
const createUserCommand = new CreateUserCommand(createUserRequest);
|
||||||
|
it('should create a new user', async () => {
|
||||||
|
UserEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
const result: AggregateID = await createUserService.execute(
|
||||||
|
createUserCommand,
|
||||||
|
);
|
||||||
|
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||||
|
});
|
||||||
|
it('should throw an error if something bad happens', async () => {
|
||||||
|
UserEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
createUserService.execute(createUserCommand),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
it('should throw an exception if User already exists', async () => {
|
||||||
|
UserEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
createUserService.execute(createUserCommand),
|
||||||
|
).rejects.toBeInstanceOf(UserAlreadyExistsException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { CreateUserProps } from '@modules/user/core/domain/user.types';
|
||||||
|
|
||||||
|
const createUserProps: CreateUserProps = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('User entity create', () => {
|
||||||
|
it('should create a new user entity', async () => {
|
||||||
|
const userEntity: UserEntity = UserEntity.create(createUserProps);
|
||||||
|
expect(userEntity.id.length).toBe(36);
|
||||||
|
expect(userEntity.getProps().email).toBe('john.doe@email.com');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,79 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
|
||||||
import { CreateUserCommand } from '../../commands/create-user.command';
|
|
||||||
import { CreateUserRequest } from '../../domain/dtos/create-user.request';
|
|
||||||
import { User } from '../../domain/entities/user';
|
|
||||||
import { CreateUserUseCase } from '../../domain/usecases/create-user.usecase';
|
|
||||||
import { UserProfile } from '../../mappers/user.profile';
|
|
||||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
const newUserRequest: CreateUserRequest = {
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
email: 'john.doe@email.com',
|
|
||||||
phone: '0601020304',
|
|
||||||
};
|
|
||||||
const newUserCommand: CreateUserCommand = new CreateUserCommand(newUserRequest);
|
|
||||||
|
|
||||||
const mockUsersRepository = {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
...newUserRequest,
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Already exists');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('CreateUserUseCase', () => {
|
|
||||||
let createUserUseCase: CreateUserUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: UsersRepository,
|
|
||||||
useValue: mockUsersRepository,
|
|
||||||
},
|
|
||||||
CreateUserUseCase,
|
|
||||||
UserProfile,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_PUBLISHER,
|
|
||||||
useValue: mockMessagePublisher,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
createUserUseCase = module.get<CreateUserUseCase>(CreateUserUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(createUserUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should create and return a new user', async () => {
|
|
||||||
const newUser: User = await createUserUseCase.execute(newUserCommand);
|
|
||||||
|
|
||||||
expect(newUser.lastName).toBe(newUserRequest.lastName);
|
|
||||||
expect(newUser.uuid).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if user already exists', async () => {
|
|
||||||
await expect(
|
|
||||||
createUserUseCase.execute(newUserCommand),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,93 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
|
||||||
import { DeleteUserCommand } from '../../commands/delete-user.command';
|
|
||||||
import { DeleteUserUseCase } from '../../domain/usecases/delete-user.usecase';
|
|
||||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
const mockUsers = [
|
|
||||||
{
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
email: 'john.doe@email.com',
|
|
||||||
phone: '0601020304',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
|
||||||
firstName: 'Jane',
|
|
||||||
lastName: 'Doe',
|
|
||||||
email: 'jane.doe@email.com',
|
|
||||||
phone: '0602030405',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
|
||||||
firstName: 'Jimmy',
|
|
||||||
lastName: 'Doe',
|
|
||||||
email: 'jimmy.doe@email.com',
|
|
||||||
phone: '0603040506',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockUsersRepository = {
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce((uuid: string) => {
|
|
||||||
let savedUser = {};
|
|
||||||
mockUsers.forEach((user, index) => {
|
|
||||||
if (user.uuid === uuid) {
|
|
||||||
savedUser = { ...user };
|
|
||||||
mockUsers.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return savedUser;
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Error');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('DeleteUserUseCase', () => {
|
|
||||||
let deleteUserUseCase: DeleteUserUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: UsersRepository,
|
|
||||||
useValue: mockUsersRepository,
|
|
||||||
},
|
|
||||||
DeleteUserUseCase,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_PUBLISHER,
|
|
||||||
useValue: mockMessagePublisher,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
deleteUserUseCase = module.get<DeleteUserUseCase>(DeleteUserUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(deleteUserUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should delete a user', async () => {
|
|
||||||
const savedUuid = mockUsers[0].uuid;
|
|
||||||
const deleteUserCommand = new DeleteUserCommand(savedUuid);
|
|
||||||
await deleteUserUseCase.execute(deleteUserCommand);
|
|
||||||
|
|
||||||
const deletedUser = mockUsers.find((user) => user.uuid === savedUuid);
|
|
||||||
expect(deletedUser).toBeUndefined();
|
|
||||||
});
|
|
||||||
it('should throw an error if user does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
deleteUserUseCase.execute(new DeleteUserCommand('wrong uuid')),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PrismaService } from '@modules/user/infrastructure/prisma.service';
|
||||||
|
import { UserRepository } from '@modules/user/infrastructure/user.repository';
|
||||||
|
import { UserMapper } from '@modules/user/user.mapper';
|
||||||
|
|
||||||
|
const mockMessagePublisher = {
|
||||||
|
publish: jest.fn().mockImplementation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('User repository', () => {
|
||||||
|
let prismaService: PrismaService;
|
||||||
|
let userMapper: UserMapper;
|
||||||
|
let eventEmitter: EventEmitter2;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [EventEmitterModule.forRoot()],
|
||||||
|
providers: [PrismaService, UserMapper],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
|
userMapper = module.get<UserMapper>(UserMapper);
|
||||||
|
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||||
|
});
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(
|
||||||
|
new UserRepository(
|
||||||
|
prismaService,
|
||||||
|
userMapper,
|
||||||
|
eventEmitter,
|
||||||
|
mockMessagePublisher,
|
||||||
|
),
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { IdResponse } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors';
|
||||||
|
import { CreateUserGrpcController } from '@modules/user/interface/dtos/grpc-controllers/create-user.grpc.controller';
|
||||||
|
import { CreateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto';
|
||||||
|
|
||||||
|
const createUserRequest: CreateUserRequestDto = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new UserAlreadyExistsException();
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Create User Grpc Controller', () => {
|
||||||
|
let createUserGrpcController: CreateUserGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
CreateUserGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
createUserGrpcController = module.get<CreateUserGrpcController>(
|
||||||
|
CreateUserGrpcController,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(createUserGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new user', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
const result: IdResponse = await createUserGrpcController.create(
|
||||||
|
createUserRequest,
|
||||||
|
);
|
||||||
|
expect(result).toBeInstanceOf(IdResponse);
|
||||||
|
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a dedicated RpcException if user already exists', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await createUserGrpcController.create(createUserRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a generic RpcException', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await createUserGrpcController.create(createUserRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { IdResponse } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
EmailAlreadyExistsException,
|
||||||
|
PhoneAlreadyExistsException,
|
||||||
|
} from '@modules/user/core/domain/user.errors';
|
||||||
|
import { UpdateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/update-user.request.dto';
|
||||||
|
import { UpdateUserGrpcController } from '@modules/user/interface/dtos/grpc-controllers/update-user.grpc.controller';
|
||||||
|
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const updateFirstNameUserRequest: UpdateUserRequestDto = {
|
||||||
|
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||||
|
firstName: 'Johnny',
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEmailUserRequest: UpdateUserRequestDto = {
|
||||||
|
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||||
|
email: 'john.doe@already.exists.email.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePhoneUserRequest: UpdateUserRequestDto = {
|
||||||
|
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||||
|
phone: '+33611223344',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => 'c97b1783-76cf-4840-b298-b90b13c58894')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new EmailAlreadyExistsException();
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new PhoneAlreadyExistsException();
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Update User Grpc Controller', () => {
|
||||||
|
let updateUserGrpcController: UpdateUserGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
UpdateUserGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
updateUserGrpcController = module.get<UpdateUserGrpcController>(
|
||||||
|
UpdateUserGrpcController,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(updateUserGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a user', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
const result: IdResponse = await updateUserGrpcController.updateUser(
|
||||||
|
updateFirstNameUserRequest,
|
||||||
|
);
|
||||||
|
expect(result).toBeInstanceOf(IdResponse);
|
||||||
|
expect(result.id).toBe('c97b1783-76cf-4840-b298-b90b13c58894');
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a dedicated RpcException if email already exists', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await updateUserGrpcController.updateUser(updateEmailUserRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a dedicated RpcException if phone already exists', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await updateUserGrpcController.updateUser(updatePhoneUserRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a generic RpcException', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await updateUserGrpcController.updateUser(updateFirstNameUserRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,36 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
|
|
||||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
const mockMessageBrokerPublisher = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Messager', () => {
|
|
||||||
let messagePublisher: MessagePublisher;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
MessagePublisher,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_BROKER_PUBLISHER,
|
|
||||||
useValue: mockMessageBrokerPublisher,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(messagePublisher).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish a message', async () => {
|
|
||||||
jest.spyOn(mockMessageBrokerPublisher, 'publish');
|
|
||||||
messagePublisher.publish('user.create.info', 'my-test');
|
|
||||||
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,84 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
|
||||||
import { UpdateUserCommand } from '../../commands/update-user.command';
|
|
||||||
import { UpdateUserRequest } from '../../domain/dtos/update-user.request';
|
|
||||||
import { User } from '../../domain/entities/user';
|
|
||||||
import { UpdateUserUseCase } from '../../domain/usecases/update-user.usecase';
|
|
||||||
import { UserProfile } from '../../mappers/user.profile';
|
|
||||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
const originalUser: User = new User();
|
|
||||||
originalUser.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
|
||||||
originalUser.lastName = 'Doe';
|
|
||||||
|
|
||||||
const updateUserRequest: UpdateUserRequest = {
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
|
||||||
lastName: 'Dane',
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUserCommand: UpdateUserCommand = new UpdateUserCommand(
|
|
||||||
updateUserRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockUsersRepository = {
|
|
||||||
update: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce((uuid: string, params: any) => {
|
|
||||||
originalUser.lastName = params.lastName;
|
|
||||||
|
|
||||||
return Promise.resolve(originalUser);
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Error');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessager = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('UpdateUserUseCase', () => {
|
|
||||||
let updateUserUseCase: UpdateUserUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: UsersRepository,
|
|
||||||
useValue: mockUsersRepository,
|
|
||||||
},
|
|
||||||
UpdateUserUseCase,
|
|
||||||
UserProfile,
|
|
||||||
{
|
|
||||||
provide: MESSAGE_PUBLISHER,
|
|
||||||
useValue: mockMessager,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
updateUserUseCase = module.get<UpdateUserUseCase>(UpdateUserUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(updateUserUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should update a user', async () => {
|
|
||||||
const updatedUser: User = await updateUserUseCase.execute(
|
|
||||||
updateUserCommand,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedUser.lastName).toBe(updateUserRequest.lastName);
|
|
||||||
});
|
|
||||||
it('should throw an error if user does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
updateUserUseCase.execute(updateUserCommand),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||||
|
import { UserModel } from '@modules/user/infrastructure/user.repository';
|
||||||
|
import { UserResponseDto } from '@modules/user/interface/dtos/user.response.dto';
|
||||||
|
import { UserMapper } from '@modules/user/user.mapper';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const now = new Date('2023-06-21 06:00:00');
|
||||||
|
const userEntity: 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 userModel: UserModel = {
|
||||||
|
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@email.com',
|
||||||
|
phone: '+33611223344',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('User Mapper', () => {
|
||||||
|
let userMapper: UserMapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
providers: [UserMapper],
|
||||||
|
}).compile();
|
||||||
|
userMapper = module.get<UserMapper>(UserMapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(userMapper).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to persistence data', async () => {
|
||||||
|
const mapped: UserModel = userMapper.toPersistence(userEntity);
|
||||||
|
expect(mapped.lastName).toBe('Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map persisted data to domain entity', async () => {
|
||||||
|
const mapped: UserEntity = userMapper.toDomain(userModel);
|
||||||
|
expect(mapped.getProps().firstName).toBe('John');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to response', async () => {
|
||||||
|
const mapped: UserResponseDto = userMapper.toResponse(userEntity);
|
||||||
|
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const USER_MESSAGE_PUBLISHER = Symbol('USER_MESSAGE_PUBLISHER');
|
||||||
|
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Mapper } from '@mobicoop/ddd-library';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { UserEntity } from './core/domain/user.entity';
|
||||||
|
import { UserModel } from './infrastructure/user.repository';
|
||||||
|
import { UserResponseDto } from './interface/dtos/user.response.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper constructs objects that are used in different layers:
|
||||||
|
* Record is an object that is stored in a database,
|
||||||
|
* Entity is an object that is used in application domain layer,
|
||||||
|
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserMapper
|
||||||
|
implements Mapper<UserEntity, UserModel, UserModel, UserResponseDto>
|
||||||
|
{
|
||||||
|
toPersistence = (entity: UserEntity): UserModel => {
|
||||||
|
const copy = entity.getProps();
|
||||||
|
const record: UserModel = {
|
||||||
|
uuid: copy.id,
|
||||||
|
firstName: copy.firstName,
|
||||||
|
lastName: copy.lastName,
|
||||||
|
email: copy.email,
|
||||||
|
phone: copy.phone,
|
||||||
|
createdAt: copy.createdAt,
|
||||||
|
updatedAt: copy.updatedAt,
|
||||||
|
};
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
toDomain = (record: UserModel): UserEntity => {
|
||||||
|
const entity = new UserEntity({
|
||||||
|
id: record.uuid,
|
||||||
|
createdAt: new Date(record.createdAt),
|
||||||
|
updatedAt: new Date(record.updatedAt),
|
||||||
|
props: {
|
||||||
|
firstName: record.firstName,
|
||||||
|
lastName: record.lastName,
|
||||||
|
email: record.email,
|
||||||
|
phone: record.phone,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
|
|
||||||
|
toResponse = (entity: UserEntity): UserResponseDto => {
|
||||||
|
const props = entity.getProps();
|
||||||
|
const response = new UserResponseDto(entity);
|
||||||
|
response.firstName = props.firstName;
|
||||||
|
response.lastName = props.lastName;
|
||||||
|
response.email = props.email;
|
||||||
|
response.phone = props.phone;
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ^ Data returned to the user is whitelisted to avoid leaks.
|
||||||
|
If a new property is added, like password or a
|
||||||
|
credit card number, it won't be returned
|
||||||
|
unless you specifically allow this.
|
||||||
|
(avoid blacklisting, which will return everything
|
||||||
|
but blacklisted items, which can lead to a data leak).
|
||||||
|
*/
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
import { Injectable, ValidationPipe } from '@nestjs/common';
|
|
||||||
import { RpcException } from '@nestjs/microservices';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RpcValidationPipe extends ValidationPipe {
|
|
||||||
createExceptionFactory() {
|
|
||||||
return (validationErrors = []) => {
|
|
||||||
return new RpcException({
|
|
||||||
code: 3,
|
|
||||||
message: this.flattenValidationErrors(validationErrors),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { ArgumentMetadata } from '@nestjs/common';
|
|
||||||
import { UpdateUserRequest } from '../../../modules/user/domain/dtos/update-user.request';
|
|
||||||
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
|
|
||||||
|
|
||||||
describe('RpcValidationPipe', () => {
|
|
||||||
it('should not validate request', async () => {
|
|
||||||
const target: RpcValidationPipe = new RpcValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
});
|
|
||||||
const metadata: ArgumentMetadata = {
|
|
||||||
type: 'body',
|
|
||||||
metatype: UpdateUserRequest,
|
|
||||||
data: '',
|
|
||||||
};
|
|
||||||
await target.transform(<UpdateUserRequest>{}, metadata).catch((err) => {
|
|
||||||
expect(err.message).toEqual('Rpc Exception');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -16,6 +16,10 @@
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"paths": {
|
||||||
|
"@modules/*": ["src/modules/*"],
|
||||||
|
"@src/*": ["src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue