wip
This commit is contained in:
parent
ee9474828e
commit
d5c2bb396d
|
@ -7,9 +7,9 @@ HEALTH_SERVICE_PORT=6001
|
|||
# PRISMA
|
||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
||||
|
||||
# RABBIT MQ
|
||||
RMQ_URI=amqp://v3-broker:5672
|
||||
RMQ_EXCHANGE=mobicoop
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=v3-redis
|
||||
|
|
|
@ -9,14 +9,13 @@
|
|||
"version": "0.0.1",
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@automapper/classes": "^8.7.7",
|
||||
"@automapper/core": "^8.7.7",
|
||||
"@automapper/nestjs": "^8.7.7",
|
||||
"@grpc/grpc-js": "^1.8.0",
|
||||
"@grpc/proto-loader": "^0.7.4",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@mobicoop/configuration-module": "^1.1.0",
|
||||
"@mobicoop/message-broker-module": "^1.0.5",
|
||||
"@mobicoop/configuration-module": "^1.2.0",
|
||||
"@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/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
|
@ -32,6 +31,7 @@
|
|||
"cache-manager": "^5.2.1",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"ioredis": "^5.3.0"
|
||||
},
|
||||
|
@ -41,6 +41,7 @@
|
|||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@ -174,39 +175,6 @@
|
|||
"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": {
|
||||
"version": "7.21.4",
|
||||
"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": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
@ -311,9 +279,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
@ -1938,12 +1906,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mobicoop/configuration-module": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.1.0.tgz",
|
||||
"integrity": "sha512-4yzCrY8m40XOO3CZnWJC4kHk66sTQCwe5UjKCV/UpNkN9IGUKW+R84J/53aulmGTL95vec7g6tFIwlHJd9BCoA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.2.0.tgz",
|
||||
"integrity": "sha512-l0iDae7SgVVmjnCa2MBqAr3Er0yn4E7yiG8e7cs4XtNGUKrC1N0Ju56TEAraEYK9aZAZ36TCs06m1fep+rgwFA==",
|
||||
"dependencies": {
|
||||
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@mobicoop/message-broker-module": "^1.0.4",
|
||||
"@nestjs/cqrs": "^9.0.4",
|
||||
"@types/amqplib": "^0.10.1",
|
||||
"amqplib": "^0.10.3",
|
||||
|
@ -1954,10 +1922,43 @@
|
|||
"@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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.0.5.tgz",
|
||||
"integrity": "sha512-9l2qCUXide2R1GzTmH1Z8CDHV0+zPJVp1OAk0q+PW9M73id6vX3j0uXURhGLQOwe01IhEMKkFs+D6YNPUPqqmw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz",
|
||||
"integrity": "sha512-RoSHHK1GyQ/QVDmm3JS/wBfh171oChvyEp6YWmJd12krFLrPVn9MoEvZdyT3I5J31oBiUabMPle5Kdpw+Nrmww==",
|
||||
"dependencies": {
|
||||
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
||||
"@types/amqplib": "^0.10.1",
|
||||
|
@ -1967,6 +1968,17 @@
|
|||
"@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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz",
|
||||
|
@ -2193,6 +2205,19 @@
|
|||
"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": {
|
||||
"version": "9.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.2.tgz",
|
||||
|
@ -2754,6 +2779,12 @@
|
|||
"@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": {
|
||||
"version": "13.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz",
|
||||
|
@ -3340,8 +3371,17 @@
|
|||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"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": {
|
||||
"version": "28.1.3",
|
||||
|
@ -4043,7 +4083,6 @@
|
|||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
|
@ -4284,7 +4323,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
@ -4764,6 +4802,11 @@
|
|||
"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": {
|
||||
"version": "3.3.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
|
@ -5228,20 +5289,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"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": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
@ -7362,9 +7409,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
@ -8133,9 +8180,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz",
|
||||
"integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
|
||||
"integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
|
@ -8172,6 +8219,11 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
|
@ -8583,9 +8635,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
|
@ -9782,9 +9834,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"engines": {
|
||||
"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",
|
||||
"pretty:check": "./node_modules/.bin/prettier --check .",
|
||||
"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:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose",
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
|
||||
"test: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/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"generate": "docker exec v3-user-api sh -c 'npx prisma generate'",
|
||||
"migrate": "docker exec v3-user-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||
"migrate:deploy": "npx prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automapper/classes": "^8.7.7",
|
||||
"@automapper/core": "^8.7.7",
|
||||
"@automapper/nestjs": "^8.7.7",
|
||||
"@grpc/grpc-js": "^1.8.0",
|
||||
"@grpc/proto-loader": "^0.7.4",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@mobicoop/configuration-module": "^1.1.0",
|
||||
"@mobicoop/message-broker-module": "^1.0.5",
|
||||
"@mobicoop/configuration-module": "^1.2.0",
|
||||
"@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/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
|
@ -54,6 +52,7 @@
|
|||
"cache-manager": "^5.2.1",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"ioredis": "^5.3.0"
|
||||
},
|
||||
|
@ -63,6 +62,7 @@
|
|||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@ -84,12 +84,12 @@
|
|||
"ts"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
|
@ -101,15 +101,19 @@
|
|||
"**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"moduleNameMapper": {
|
||||
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||
"^@src(.*)": "<rootDir>$1"
|
||||
},
|
||||
"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 { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import {
|
||||
ConfigurationModule,
|
||||
ConfigurationModuleOptions,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import {
|
||||
MessageBrokerModule,
|
||||
MessageBrokerModuleOptions,
|
||||
} from '@mobicoop/message-broker-module';
|
||||
HealthModule,
|
||||
HealthModuleOptions,
|
||||
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({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
||||
UserModule,
|
||||
MessageBrokerModule.forRootAsync(
|
||||
{
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<MessageBrokerModuleOptions> => ({
|
||||
ConfigurationModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<ConfigurationModuleOptions> => ({
|
||||
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||
messageBroker: {
|
||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||
handlers: {},
|
||||
}),
|
||||
},
|
||||
false,
|
||||
),
|
||||
ConfigurationModule.forRootAsync(
|
||||
{
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<ConfigurationModuleOptions> => ({
|
||||
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||
messageBroker: {
|
||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||
},
|
||||
redis: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
},
|
||||
setConfigurationBrokerQueue: 'user-configuration-create-update',
|
||||
deleteConfigurationQueue: 'user-configuration-delete',
|
||||
propagateConfigurationQueue: 'user-configuration-propagate',
|
||||
}),
|
||||
}),
|
||||
HealthModule.forRootAsync({
|
||||
imports: [UserModule, MessagerModule],
|
||||
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],
|
||||
useFactory: async (
|
||||
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'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
},
|
||||
setConfigurationBrokerRoutingKeys: [
|
||||
'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,
|
||||
],
|
||||
messagePublisher,
|
||||
}),
|
||||
}),
|
||||
UserModule,
|
||||
MessagerModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
exports: [UserModule, MessagerModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
|||
|
||||
package health;
|
||||
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
|||
NOT_SERVING = 2;
|
||||
}
|
||||
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: {
|
||||
package: ['user', 'health'],
|
||||
protoPath: [
|
||||
join(__dirname, 'modules/user/adapters/primaries/user.proto'),
|
||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
||||
join(__dirname, 'modules/user/interface/grpc-controllers/user.proto'),
|
||||
join(__dirname, 'health.proto'),
|
||||
],
|
||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||
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,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"paths": {
|
||||
"@modules/*": ["src/modules/*"],
|
||||
"@src/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue