diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42d24d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.vscode/ +node_modules/ +dist/ +npm-debug.log \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9650f87 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +NODE_ENV=development +APP_NAME="WPCAS Survey API" +API_PREFIX=api +APP_PORT= + +DATABASE_USERNAME= +DATABASE_PASSWORD= +DATABASE_NAME= +TELEMETRY_DATABASE_NAME= +DATABASE_PORT= +PASSBOOK_SERVICE_URL= +SUNBIRD_SERVICE_URL= +DATABASE_URL=postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@172.17.0.1:${DATABASE_PORT}/${DATABASE_NAME}?schema=public +USER_SERVICE_TOKEN= +USER_SERVICE_COOKIE= +USER_SERVICE_URL= +FRAC_SERVICE_URL= +##expression configured to trigger the CRON job EVERY_WEEKEND +FRAC_CRON_EPX="0 0 * * 6,0" +USER_CRON_EPX="0 0 * * 6,0" diff --git a/.gitignore b/.gitignore index f6a091e..fc919cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # compiled output dist node_modules +.data # enviornment variables .env diff --git a/Dockerfile b/Dockerfile index 7428982..953a06b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ + +# Use the official Node.js 18 image as a base FROM node:18.16.1-alpine +# Install bash for debugging purposes (optional) RUN apk add --no-cache bash + +# Install global Node.js packages for NestJS development RUN npm i -g @nestjs/cli typescript ts-node # Set the working directory inside the container @@ -9,33 +14,23 @@ WORKDIR /usr/src/app # Copy package.json and package-lock.json to the working directory COPY package*.json ./ +# Copy the Prisma configuration and migration files +# This line copies the "prisma" directory from your project's root into the Docker container's working directory. +COPY prisma ./prisma/ +COPY env-example ./.env # Install project dependencies RUN npm install # Copy the rest of the application code to the container COPY . . -# Expose the PORT environment variable (default to 4000 if not provided) -ARG PORT=4010 -ENV PORT=$APP_PORT - - -# COPY package*.json /tmp/app/ -# RUN cd /tmp/app && npm install - -# COPY . /usr/src/app -# RUN cp -a /tmp/app/node_modules /usr/src/app -# COPY ./wait-for-it.sh /opt/wait-for-it.sh -# COPY ./startup.dev.sh /opt/startup.dev.sh -# RUN sed -i 's/\r//g' /opt/wait-for-it.sh -# RUN sed -i 's/\r//g' /opt/startup.dev.sh - -# RUN cp env-example .env - # Build your Nest.js application RUN npm run build +# Expose the PORT environment variable (default to 4000 if not provided) +ARG PORT=4000 +ENV PORT=$PORT +EXPOSE $PORT + # Start the Nest.js application using the start:prod script CMD ["npm", "run", "start:prod"] - -# CMD ["/opt/startup.dev.sh"] diff --git a/README.md b/README.md index 6aef540..38be4c7 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ -# Microservice boilerplate +# WPCAS - Workplace Competency Assessment Score + +## About + +WPCAS is a vital component of COMPASS, a goal-oriented human resource management system (GO-HRM) designed to help organizations align their objectives with well-defined targets for teams and individuals. This system maps competencies required to achieve these targets and establishes a connection between capacity and performance management. + +WPCAS specifically focuses on Workplace Competency Assessment Scores, offering a comprehensive 360-degree feedback survey for employees. This survey involves input from juniors, seniors, and colleagues, providing valuable insights into an employee's strengths and weaknesses based on workplace performance. + +## Tech Stack + +- **Language:** TypeScript +- **Database:** PostgreSQL +- **ORM:** Prisma +- **Framework:** NestJS +- **Node Version:** 18.16.1-alpine + +## Setup + +1. Install the necessary package dependencies: + ```bash + npm i + ``` + +2. Set up PostgreSQL in your local environment. + +3. Configure environment variables: + - Create an environment variable file (e.g., `.env`) using the example file as a reference. + +4. Generate Prisma migrations: + ```bash + npx prisma migrate dev + ``` + - If seed data is required, populate it by running: + ```bash + npx prisma db seed + ``` + or for a complete reset (including deleting all previous data): + ```bash + npx prisma migrate reset + ``` + +5. Running a Local Development Server: + ```bash + npm run start:dev + ``` + - Access the Swagger API documentation at `http://YOUR_APP_PORT/api/docs` + +## License + +This project is licensed under the [LICENSE NAME] - see the [LICENSE.md](LICENSE.md) file for details. \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 56df483..200a4b9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,38 +1,27 @@ -services: - postgres: - image: postgres:15.3-alpine - ports: - - ${DATABASE_PORT}:5432 - volumes: - - ./.data/db:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${DATABASE_USERNAME} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: ${DATABASE_NAME} - - # maildev: - # build: - # context: . - # dockerfile: maildev.Dockerfile - # ports: - # - ${MAIL_CLIENT_PORT}:1080 - # - ${MAIL_PORT}:1025 - - # adminer: - # image: adminer - # restart: always - # ports: - # - 8080:8080 - - # Uncomment to use redis - # redis: - # image: redis:7-alpine - # ports: - # - 6379:6379 +version: '3' +services: api: build: context: . dockerfile: Dockerfile ports: - ${APP_PORT}:${APP_PORT} + networks: + - samagra_compass + +networks: + samagra_compass: + external: true + +# postgres: +# image: postgres:15.3-alpine +# ports: +# - ${DATABASE_PORT}:5432 +# volumes: +# - ./.data/db:/var/lib/postgresql/data +# environment: +# POSTGRES_USER: ${DATABASE_USERNAME} +# POSTGRES_PASSWORD: ${DATABASE_PASSWORD} +# POSTGRES_DB: ${DATABASE_NAME} + diff --git a/e2e.Dockerfile b/e2e.Dockerfile index bbb18cd..a980ef4 100644 --- a/e2e.Dockerfile +++ b/e2e.Dockerfile @@ -20,9 +20,13 @@ # CMD ["/opt/startup.ci.sh"] +# Use the official Node.js 18 image as a base FROM node:18.16.1-alpine +# Install bash for debugging purposes (optional) RUN apk add --no-cache bash + +# Install global Node.js packages for NestJS development RUN npm i -g @nestjs/cli typescript ts-node # Set the working directory inside the container @@ -31,18 +35,23 @@ WORKDIR /usr/src/app # Copy package.json and package-lock.json to the working directory COPY package*.json ./ +# Copy the Prisma configuration and migration files +# This line copies the "prisma" directory from your project's root into the Docker container's working directory. +COPY prisma ./prisma/ + # Install project dependencies RUN npm install # Copy the rest of the application code to the container COPY . . -# Expose the PORT environment variable (default to 4000 if not provided) -ARG PORT=4010 -ENV PORT=$APP_PORT - # Build your Nest.js application RUN npm run build +# Expose the PORT environment variable (default to 4000 if not provided) +ARG PORT=4000 +ENV PORT=$PORT +EXPOSE $PORT + # Start the Nest.js application using the start:prod script CMD ["npm", "run", "start:prod"] diff --git a/env-example b/env-example deleted file mode 100644 index e3eda1f..0000000 --- a/env-example +++ /dev/null @@ -1,10 +0,0 @@ -NODE_ENV=development -APP_NAME="WPCAS Survey API" -API_PREFIX=api -APP_PORT=4010 - -DATABASE_USERNAME=postgres -DATABASE_PASSWORD=secret -DATABASE_NAME=service-db -DATABASE_PORT=5432 -DATABASE_URL="" diff --git a/package-lock.json b/package-lock.json index 7cbd9fa..63a8703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,16 @@ "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/schedule": "^3.0.4", "@nestjs/swagger": "^7.1.11", - "@prisma/client": "^5.4.1", + "@prisma/client": "^5.4.2", + "axios": "^1.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "csv-parser": "^3.0.0", + "lodash": "^4.17.21", + "multer": "^1.4.5-lts.1", + "pg": "^8.12.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -27,6 +33,8 @@ "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.14.200", + "@types/multer": "^1.4.9", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -35,8 +43,9 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", + "pactum": "^3.5.1", "prettier": "^2.3.2", - "prisma": "^5.4.1", + "prisma": "^5.4.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", @@ -252,6 +261,15 @@ "node": ">=8" } }, + "node_modules/@arr/every": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", + "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -734,9 +752,9 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", - "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", @@ -925,6 +943,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -2212,17 +2236,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/@nestjs/platform-express/node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/@nestjs/platform-express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2267,19 +2280,25 @@ "node": ">= 0.8" } }, - "node_modules/@nestjs/platform-express/node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@nestjs/platform-express/node_modules/tslib": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" }, + "node_modules/@nestjs/schedule": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-3.0.4.tgz", + "integrity": "sha512-uFJpuZsXfpvgx2y7/KrIZW9e1L68TLiwRodZ6+Gc8xqQiHSUzAVn+9F4YMxWFlHITZvvkjWziUFgRNCitDcTZQ==", + "dependencies": { + "cron": "2.4.3", + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.12" + } + }, "node_modules/@nestjs/schematics": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.2.0.tgz", @@ -2491,13 +2510,19 @@ "node": ">=8" } }, + "node_modules/@polka/url": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz", + "integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==", + "dev": true + }, "node_modules/@prisma/client": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.4.1.tgz", - "integrity": "sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.4.2.tgz", + "integrity": "sha512-2xsPaz4EaMKj1WS9iW6MlPhmbqtBsXAOeVttSePp8vTFTtvzh2hZbDgswwBdSCgPzmmwF+tLB259QzggvCmJqA==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f" + "@prisma/engines-version": "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574" }, "engines": { "node": ">=16.13" @@ -2512,16 +2537,16 @@ } }, "node_modules/@prisma/engines": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.4.1.tgz", - "integrity": "sha512-vJTdY4la/5V3N7SFvWRmSMUh4mIQnyb/MNoDjzVbh9iLmEC+uEykj/1GPviVsorvfz7DbYSQC4RiwmlEpTEvGA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.4.2.tgz", + "integrity": "sha512-fqeucJ3LH0e1eyFdT0zRx+oETLancu5+n4lhiYECyEz6H2RDskPJHJYHkVc0LhkU4Uv7fuEnppKU3nVKNzMh8g==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f.tgz", - "integrity": "sha512-+nUQM/y8C+1GG5Ioeqcu6itFslCfxvQSAUVSMC9XM2G2Fcq0F4Afnp6m0pXF6X6iUBWen7jZBPmM9Qlq4Nr3/A==" + "version": "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574.tgz", + "integrity": "sha512-wvupDL4AA1vf4TQNANg7kR7y98ITqPsk6aacfBxZKtrJKRIsWjURHkZCGcQliHdqCiW/hGreO6d6ZuSv9MhdAA==" }, "node_modules/@sinclair/typebox": { "version": "0.24.51", @@ -2735,12 +2760,32 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.200", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", + "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", + "dev": true + }, + "node_modules/@types/luxon": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.3.tgz", + "integrity": "sha512-/BJF3NT0pRMuxrenr42emRUF67sXwcZCd+S1ksG/Fcf9O7C3kKCY4uJSbKBE4KDUIYr3WMsvfmWD8hRjXExBJQ==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "node_modules/@types/multer": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.9.tgz", + "integrity": "sha512-9NSvPJ2E8bNTc8XtJq1Cimx2Wrn2Ah48F15B2Du/hM8a8CHLhVbJMlF3ZCqhvMdht7Sa+YdP0aKP7N4fxDcrrg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "16.18.57", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.57.tgz", @@ -3505,8 +3550,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "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", @@ -3896,6 +3950,17 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3954,6 +4019,12 @@ } ] }, + "node_modules/centra": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz", + "integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==", + "dev": true + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4138,7 +4209,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" }, @@ -4314,6 +4384,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-2.4.3.tgz", + "integrity": "sha512-YBvExkQYF7w0PxyeFLRyr817YVDhGxaCi5/uRRMqa4aWD3IFKRd+uNbpW1VWMdqQy8PZ7CElc+accXJcauPKzQ==", + "dependencies": { + "@types/luxon": "~3.3.0", + "luxon": "~3.3.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4328,6 +4407,20 @@ "node": ">= 8" } }, + "node_modules/csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -4340,6 +4433,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "node_modules/deep-override": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-override/-/deep-override-1.0.2.tgz", + "integrity": "sha512-+bAuLuYqaVVUWPaq8rmU8NLTX85p4I5k5/cVdhBioEfH7k+5NlGdv4NoJVQcJRByqzzTWWzTpih+pU1wBTmMow==", + "dev": true + }, "node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", @@ -4362,7 +4461,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4663,27 +4761,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-scope/node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-scope/node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", @@ -5279,6 +5356,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "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", @@ -5377,6 +5473,30 @@ "node": ">=8" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-lite": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/form-data-lite/-/form-data-lite-1.0.3.tgz", + "integrity": "sha512-P7xPqAiOPKzC9Q9aywAZJCQq4QOE5WokPb3HrcWRh7C57RKytueJzoORZAVgHBNvK/lL7E+FxjQjd4X/zbecEQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-lite": "^1.0.3" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7798,6 +7918,15 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-query": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json-query/-/json-query-2.2.2.tgz", + "integrity": "sha512-y+IcVZSdqNmS4fO8t1uZF6RMMs0xh3SrTjJr9bp1X3+v0Q13+7Cyv12dSmKwDswp/H427BVtpkLWhGxYu3ZWRA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7849,6 +7978,15 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7876,6 +8014,12 @@ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.15.tgz", "integrity": "sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg==" }, + "node_modules/lightcookie": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/lightcookie/-/lightcookie-1.0.25.tgz", + "integrity": "sha512-SrY/+eBPaKAMnsn7mCsoOMZzoQyCyHHHZlFCu2fjo28XxSyCLjlooKiTxyrXTg8NPaHp1YzWi0lcGG1gDi6KHw==", + "dev": true + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8018,6 +8162,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "engines": { + "node": ">=12" + } + }, "node_modules/macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", @@ -8072,6 +8224,18 @@ "tmpl": "1.0.5" } }, + "node_modules/matchit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz", + "integrity": "sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==", + "dev": true, + "dependencies": { + "@arr/every": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -8152,6 +8316,12 @@ "node": ">= 0.6" } }, + "node_modules/mime-lite": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mime-lite/-/mime-lite-1.0.3.tgz", + "integrity": "sha512-V85l97zJSTG8FEvmdTlmNYb0UMrVBwvRjw7bWTf/aT6KjFwtz3iTz8D2tuFIp7lwiaO2C5ecnrEmSkkMRCrqVw==", + "dev": true + }, "node_modules/mime-types": { "version": "2.1.27", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", @@ -8213,6 +8383,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -8381,6 +8568,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-fuzzer-core": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/openapi-fuzzer-core/-/openapi-fuzzer-core-1.0.6.tgz", + "integrity": "sha512-FJNJIfgUFuv4NmVGq9MYdoKra2GrkDy2uhIjE2YGlw30UA1glf4SXLMhI4UwdcJ8jisKdIxi7lXrfej8GvNW5w==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -8555,6 +8751,34 @@ "node": ">=6" } }, + "node_modules/pactum": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/pactum/-/pactum-3.5.1.tgz", + "integrity": "sha512-7DcbzGFZkvpdETtyxyB5inhmxFhXh+D/AZwF8fQaxywums4zf1nZO/vNDueogN8UWV1YldUEkuGo+1XlGvz+iQ==", + "dev": true, + "dependencies": { + "@exodus/schemasafe": "^1.2.3", + "deep-override": "^1.0.2", + "form-data-lite": "^1.0.3", + "json-query": "^2.2.2", + "klona": "^2.0.6", + "lightcookie": "^1.0.25", + "openapi-fuzzer-core": "^1.0.6", + "pactum-matchers": "^1.1.6", + "parse-graphql": "^1.0.0", + "phin": "^3.7.0", + "polka": "^0.5.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pactum-matchers": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/pactum-matchers/-/pactum-matchers-1.1.6.tgz", + "integrity": "sha512-55io32NeOKbLpHKKPzYDOr+N2dseTzMbj1Gj1y+zvOkKK6NDf5BT5pxglfqLN/ra3ig5zvbrKFUqZIWjAWboog==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8567,6 +8791,12 @@ "node": ">=6" } }, + "node_modules/parse-graphql": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-graphql/-/parse-graphql-1.0.0.tgz", + "integrity": "sha512-NjvQHHaiPCxPZrhm/kKnorxOv7r/eA+tE0VW5E8iJMH9wTqFA1V0YK/7nbpxVu3JdXUxyWTKMez9lsHUtAwa0w==", + "dev": true + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -8673,6 +8903,99 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/phin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.0.tgz", + "integrity": "sha512-DqnVNrpYhKGBZppNKprD+UJylMeEKOZxHgPB+ZP6mGzf3uA2uox4Ep9tUm+rUc8WLIdHT3HcAE4X8fhwQA9JKg==", + "dev": true, + "dependencies": { + "centra": "^2.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -8773,6 +9096,51 @@ "node": ">=4" } }, + "node_modules/polka": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz", + "integrity": "sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==", + "dev": true, + "dependencies": { + "@polka/url": "^0.5.0", + "trouter": "^2.0.1" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8837,13 +9205,13 @@ } }, "node_modules/prisma": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.4.1.tgz", - "integrity": "sha512-op9PmU8Bcw5dNAas82wBYTG0yHnpq9/O3bhxbDBrNzwZTwBqsVCxxYRLf6wHNh9HVaDGhgjjHlu1+BcW8qdnBg==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.4.2.tgz", + "integrity": "sha512-GDMZwZy7mysB2oXU+angQqJ90iaPFdD0rHaZNkn+dio5NRkGLmMqmXs31//tg/qXT3iB0cTQwnGGQNuirhSTZg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.4.1" + "@prisma/engines": "5.4.2" }, "bin": { "prisma": "build/index.js" @@ -8882,6 +9250,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", @@ -9340,6 +9713,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9375,6 +9756,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9474,20 +9863,6 @@ } } }, - "node_modules/supertest/node_modules/form-data": { - "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", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/supertest/node_modules/formidable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", @@ -9789,6 +10164,18 @@ "tree-kill": "cli.js" } }, + "node_modules/trouter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz", + "integrity": "sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==", + "dev": true, + "dependencies": { + "matchit": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-jest": { "version": "28.0.8", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", @@ -10245,6 +10632,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index fd5d923..5664e15 100644 --- a/package.json +++ b/package.json @@ -12,23 +12,35 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "npx prisma migrate deploy && node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "migrate:dev": "npx prisma migrate dev", + "migrate:dev:create": "npx prisma migrate dev --create-only", + "migrate:deploy": "npx prisma migrate deploy", + "prisma:generate": "npx prisma generate", + "prisma:studio": "npx prisma studio", + "prisma:seed": "npx prisma db seed" }, "dependencies": { "@nestjs/common": "^9.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/schedule": "^3.0.4", "@nestjs/swagger": "^7.1.11", - "@prisma/client": "^5.4.1", + "@prisma/client": "^5.4.2", + "axios": "^1.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "csv-parser": "^3.0.0", + "lodash": "^4.17.21", + "multer": "^1.4.5-lts.1", + "pg": "^8.12.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -39,6 +51,8 @@ "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.14.200", + "@types/multer": "^1.4.9", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -47,8 +61,9 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", + "pactum": "^3.5.1", "prettier": "^2.3.2", - "prisma": "^5.4.1", + "prisma": "^5.4.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", @@ -73,5 +88,8 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "prisma": { + "seed": "ts-node ./prisma/seeds/seed.ts" } } diff --git a/prisma/migrations/20231213113943_/migration.sql b/prisma/migrations/20231213113943_/migration.sql new file mode 100644 index 0000000..835e0ac --- /dev/null +++ b/prisma/migrations/20231213113943_/migration.sql @@ -0,0 +1,263 @@ +-- CreateEnum +CREATE TYPE "SurveyStatusEnum" AS ENUM ('CREATED', 'PUBLISHED', 'CLOSED'); + +-- CreateEnum +CREATE TYPE "ResponseTrackerStatusEnum" AS ENUM ('PENDING', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "TimeUnitsEnum" AS ENUM ('DAY', 'MONTH', 'YEAR'); + +-- CreateEnum +CREATE TYPE "UserRolesEnum" AS ENUM ('ADMIN', 'CONSUMER'); + +-- CreateTable +CREATE TABLE "UserMetadata" ( + "id" SERIAL NOT NULL, + "userId" UUID NOT NULL, + "userName" TEXT NOT NULL, + "profilePicture" TEXT, + "isNewEmployee" BOOLEAN NOT NULL, + "designation" TEXT, + "dateOfJoining" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "isAdmin" BOOLEAN NOT NULL, + + CONSTRAINT "UserMetadata_pkey" PRIMARY KEY ("id","userId") +); + +-- CreateTable +CREATE TABLE "question_bank" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "competencyId" INTEGER NOT NULL, + "competencyLevelNumber" INTEGER NOT NULL, + "question" TEXT NOT NULL, + + CONSTRAINT "question_bank_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "admin_competencies" ( + "id" SERIAL NOT NULL, + "competencyId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "competencyLevels" JSONB[], + + CONSTRAINT "admin_competencies_pkey" PRIMARY KEY ("id","competencyId") +); + +-- CreateTable +CREATE TABLE "survey_config" ( + "id" SERIAL NOT NULL, + "surveyName" TEXT NOT NULL, + "onboardingTime" INTEGER NOT NULL DEFAULT 3, + "onboardingTimeUnit" "TimeUnitsEnum" NOT NULL DEFAULT 'MONTH', + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "survey_config_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserMapping" ( + "id" SERIAL NOT NULL, + "surveyConfigId" INTEGER NOT NULL, + "assesseeId" UUID NOT NULL, + "assessorIds" UUID[], + + CONSTRAINT "UserMapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "survey_form" ( + "id" SERIAL NOT NULL, + "userId" UUID NOT NULL, + "questionsJson" JSONB NOT NULL, + "surveyConfigId" INTEGER NOT NULL, + "status" "SurveyStatusEnum" NOT NULL, + "overallScore" DOUBLE PRECISION, + "sunbirdCredentialIds" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "survey_form_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "response_tracker" ( + "id" SERIAL NOT NULL, + "surveyFormId" INTEGER NOT NULL, + "assesseeId" UUID NOT NULL, + "assessorId" UUID NOT NULL, + "responseJson" JSONB[] DEFAULT ARRAY[]::JSONB[], + "status" "ResponseTrackerStatusEnum" NOT NULL, + + CONSTRAINT "response_tracker_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "survey_scores" ( + "id" SERIAL NOT NULL, + "surveyFormId" INTEGER NOT NULL, + "competencyId" INTEGER NOT NULL, + "competencyLevelNumber" INTEGER NOT NULL, + "score" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "survey_scores_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "UserRolesEnum" NOT NULL, + "userName" TEXT NOT NULL, + "password" TEXT NOT NULL, + "profilePicture" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "designation" TEXT NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Designation" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + + CONSTRAINT "Designation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "roles" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "designationId" INTEGER, + + CONSTRAINT "roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "competencies" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "competencies_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RoleToCompetency" ( + "roleId" INTEGER NOT NULL, + "competencyId" INTEGER NOT NULL, + + CONSTRAINT "RoleToCompetency_pkey" PRIMARY KEY ("roleId","competencyId") +); + +-- CreateTable +CREATE TABLE "competency_levels" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "levelNumber" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "competency_levels_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CompetencyToCompetencyLevel" ( + "competencyId" INTEGER NOT NULL, + "competencyLevelId" INTEGER NOT NULL, + + CONSTRAINT "CompetencyToCompetencyLevel_pkey" PRIMARY KEY ("competencyId","competencyLevelId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserMetadata_id_key" ON "UserMetadata"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserMetadata_userId_key" ON "UserMetadata"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserMetadata_userName_key" ON "UserMetadata"("userName"); + +-- CreateIndex +CREATE UNIQUE INDEX "question_bank_competencyId_competencyLevelNumber_question_key" ON "question_bank"("competencyId", "competencyLevelNumber", "question"); + +-- CreateIndex +CREATE UNIQUE INDEX "admin_competencies_competencyId_key" ON "admin_competencies"("competencyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "survey_form_userId_surveyConfigId_key" ON "survey_form"("userId", "surveyConfigId"); + +-- CreateIndex +CREATE UNIQUE INDEX "response_tracker_surveyFormId_assesseeId_assessorId_key" ON "response_tracker"("surveyFormId", "assesseeId", "assessorId"); + +-- CreateIndex +CREATE UNIQUE INDEX "survey_scores_surveyFormId_competencyId_competencyLevelNumb_key" ON "survey_scores"("surveyFormId", "competencyId", "competencyLevelNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Designation_name_key" ON "Designation"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "competencies_name_key" ON "competencies"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "competency_levels_name_key" ON "competency_levels"("name"); + +-- AddForeignKey +ALTER TABLE "UserMapping" ADD CONSTRAINT "UserMapping_surveyConfigId_fkey" FOREIGN KEY ("surveyConfigId") REFERENCES "survey_config"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "survey_form" ADD CONSTRAINT "survey_form_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserMetadata"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "survey_form" ADD CONSTRAINT "survey_form_surveyConfigId_fkey" FOREIGN KEY ("surveyConfigId") REFERENCES "survey_config"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "response_tracker" ADD CONSTRAINT "response_tracker_surveyFormId_fkey" FOREIGN KEY ("surveyFormId") REFERENCES "survey_form"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "response_tracker" ADD CONSTRAINT "response_tracker_assesseeId_fkey" FOREIGN KEY ("assesseeId") REFERENCES "UserMetadata"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "response_tracker" ADD CONSTRAINT "response_tracker_assessorId_fkey" FOREIGN KEY ("assessorId") REFERENCES "UserMetadata"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "survey_scores" ADD CONSTRAINT "survey_scores_surveyFormId_fkey" FOREIGN KEY ("surveyFormId") REFERENCES "survey_form"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "roles" ADD CONSTRAINT "roles_designationId_fkey" FOREIGN KEY ("designationId") REFERENCES "Designation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RoleToCompetency" ADD CONSTRAINT "RoleToCompetency_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RoleToCompetency" ADD CONSTRAINT "RoleToCompetency_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "competencies"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CompetencyToCompetencyLevel" ADD CONSTRAINT "CompetencyToCompetencyLevel_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "competencies"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CompetencyToCompetencyLevel" ADD CONSTRAINT "CompetencyToCompetencyLevel_competencyLevelId_fkey" FOREIGN KEY ("competencyLevelId") REFERENCES "competency_levels"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20231214105545_credential_database/migration.sql b/prisma/migrations/20231214105545_credential_database/migration.sql new file mode 100644 index 0000000..2de7557 --- /dev/null +++ b/prisma/migrations/20231214105545_credential_database/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "credential_did" ( + "id" SERIAL NOT NULL, + "authorDid" TEXT NOT NULL, + "schemaDid" TEXT NOT NULL, + "schemaVersion" TEXT NOT NULL, + + CONSTRAINT "credential_did_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "credential_did_schemaDid_schemaVersion_key" ON "credential_did"("schemaDid", "schemaVersion"); diff --git a/prisma/migrations/20240405124851_adding_activities_column/migration.sql b/prisma/migrations/20240405124851_adding_activities_column/migration.sql new file mode 100644 index 0000000..42c33d1 --- /dev/null +++ b/prisma/migrations/20240405124851_adding_activities_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "roles" ADD COLUMN "activities" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/prisma/migrations/20240416181904_fix_designation_to_role_mapping/migration.sql b/prisma/migrations/20240416181904_fix_designation_to_role_mapping/migration.sql new file mode 100644 index 0000000..5f4d0a8 --- /dev/null +++ b/prisma/migrations/20240416181904_fix_designation_to_role_mapping/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `designationId` on the `roles` table. All the data in the column will be lost. + - You are about to drop the `Designation` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "roles" DROP CONSTRAINT "roles_designationId_fkey"; + +-- AlterTable +ALTER TABLE "roles" DROP COLUMN "designationId"; + +-- DropTable +DROP TABLE "Designation"; + +-- CreateTable +CREATE TABLE "designations" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + + CONSTRAINT "designations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DesignationToRole" ( + "designationId" INTEGER NOT NULL, + "roleId" INTEGER NOT NULL, + + CONSTRAINT "DesignationToRole_pkey" PRIMARY KEY ("designationId","roleId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "designations_name_key" ON "designations"("name"); + +-- AddForeignKey +ALTER TABLE "DesignationToRole" ADD CONSTRAINT "DesignationToRole_designationId_fkey" FOREIGN KEY ("designationId") REFERENCES "designations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesignationToRole" ADD CONSTRAINT "DesignationToRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 78e02ce..34390a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,11 +10,286 @@ datasource db { url = env("DATABASE_URL") } -//dummy user model +// Here goes models +model UserMetadata { + id Int @unique() @default(autoincrement()) + userId String @unique() @db.Uuid + userName String @unique() + profilePicture String? + isNewEmployee Boolean + designation String? + dateOfJoining DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + SurveyForm SurveyForm[] + isAdmin Boolean + ResponseTrackersAsAssessee ResponseTracker[] @relation("AssesseeResponseTrackers") + ResponseTrackersAsAssessor ResponseTracker[] @relation("AssessorResponseTrackers") + // departmentId Int + // Department AdminDepartment @relation(fields: [departmentId], references: [departmentId]) + + @@id([id, userId]) +} + +model QuestionBank { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + competencyId Int + competencyLevelNumber Int + question String + + @@unique([competencyId, competencyLevelNumber, question]) + @@map("question_bank") +} + +model AdminCompetency { + id Int @default(autoincrement()) + competencyId Int @unique + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + competencyLevels Json[] //{competencyLevelNumber, competencyLevelName} + + @@id([id, competencyId]) + @@map("admin_competencies") +} + +// model AdminDepartment { +// id Int @default(autoincrement()) +// departmentId Int @unique +// name String +// description String? +// UserMetadata UserMetadata[] +// SurveyConfig SurveyConfig? + +// @@id([id, departmentId]) +// @@map("admin_department") +// } + +model CredentialDid { + id Int @id @default(autoincrement()) + authorDid String + schemaDid String + schemaVersion String + + @@unique([schemaDid, schemaVersion]) + @@map("credential_did") +} + +model SurveyConfig { + id Int @id @default(autoincrement()) + surveyName String + onboardingTime Int @default(3) + onboardingTimeUnit TimeUnitsEnum @default(MONTH) + startTime DateTime + endTime DateTime + isActive Boolean @default(false) + SurveyForm SurveyForm[] + UserMapping UserMapping[] + // departmentId Int @unique + // AdminDepartment AdminDepartment @relation(fields: [departmentId], references: [departmentId]) + // SurveyCycleParameters SurveyCycleParameter[] + + @@map("survey_config") +} + +model UserMapping { + id Int @id @default(autoincrement()) + surveyConfigId Int + SurveyConfig SurveyConfig @relation(fields: [surveyConfigId], references: [id], onDelete: Cascade) + assesseeId String @db.Uuid + assessorIds String[] @db.Uuid +} + +// model SurveyCycleParameter { +// id Int @id @default(autoincrement()) +// startTime DateTime +// endTime DateTime +// surveyForm SurveyForm[] +// isActive Boolean +// surveyConfigId Int? +// SurveyConfig SurveyConfig? @relation(fields: [surveyConfigId], references: [id]) +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt + +// @@map("survey_cycle_parameters") +// } + +model SurveyForm { + id Int @id @default(autoincrement()) + userId String @db.Uuid + UserMetadata UserMetadata @relation(fields: [userId], references: [userId]) + questionsJson Json //{questionId: question} + surveyConfigId Int + SurveyConfig SurveyConfig @relation(fields: [surveyConfigId], references: [id]) + status SurveyStatusEnum + overallScore Float? + sunbirdCredentialIds String? + ResponseTracker ResponseTracker[] + SurveyScore SurveyScore[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // surveyCycleParameterId Int + // surveyCycleParameter SurveyCycleParameter @relation(fields: [surveyCycleParameterId], references: [id]) + + @@unique([userId, surveyConfigId]) + @@map("survey_form") +} + +model ResponseTracker { + id Int @id @default(autoincrement()) + surveyFormId Int + surveyForm SurveyForm @relation(fields: [surveyFormId], references: [id], onDelete: Cascade) + assesseeId String @db.Uuid + assessorId String @db.Uuid + responseJson Json[] @default([]) //{questionId: number, answer: (Yes, No and DoNotKnow)} + status ResponseTrackerStatusEnum + Assessee UserMetadata @relation("AssesseeResponseTrackers", fields: [assesseeId], references: [userId]) + Assessor UserMetadata @relation("AssessorResponseTrackers", fields: [assessorId], references: [userId]) + + @@unique([surveyFormId, assesseeId, assessorId]) + @@map("response_tracker") +} + +model SurveyScore { + id Int @id @default(autoincrement()) + surveyFormId Int + surveyForm SurveyForm @relation(fields: [surveyFormId], references: [id]) + competencyId Int + competencyLevelNumber Int + score Float + + @@unique([surveyFormId, competencyId, competencyLevelNumber]) + @@map("survey_scores") +} + +enum SurveyStatusEnum { + CREATED + PUBLISHED + CLOSED +} + +enum ResponseTrackerStatusEnum { + PENDING + COMPLETED +} + +enum TimeUnitsEnum { + DAY + MONTH + YEAR +} + +//Mock Database Models - To be removed after the app is integrated with User Service and FRAC Based Competency Tagger. model User { - id Int @id @default(autoincrement()) - email String @unique - name String? + id String @id @default(uuid()) + email String @unique + role UserRolesEnum + userName String + password String + profilePicture String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + designation String + // Level Level? @relation(fields: [levelNumber], references: [levelNumber]) + // levelNumber Int? + // Department Department? @relation(fields: [departmentId], references: [id]) + // departmentId Int? + + @@map("users") } -// Here goes models +// model Level { +// levelNumber Int @id @unique +// description String? +// users User[] +// CompetencyLevel CompetencyLevel[] +// } + +// model Department { +// id Int @id @default(autoincrement()) +// name String @unique +// description String? +// users User[] +// } + +model Designation { + id Int @id @default(autoincrement()) + name String @unique + description String? + roles DesignationToRole[] + + @@map("designations") +} + +model Role { + id Int @id @default(autoincrement()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + competencies RoleToCompetency[] + designations DesignationToRole[] + activities String[] @default([]) + + @@map("roles") +} + +model DesignationToRole { + designation Designation @relation(fields: [designationId], references: [id], onDelete: Cascade) + designationId Int + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + roleId Int + + @@id([designationId, roleId]) +} + +model Competency { + id Int @id @default(autoincrement()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + roles RoleToCompetency[] + competencyLevels CompetencyToCompetencyLevel[] + + @@map("competencies") +} + +model RoleToCompetency { + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + roleId Int + competency Competency @relation(fields: [competencyId], references: [id], onDelete: Cascade) + competencyId Int + + @@id([roleId, competencyId]) +} + +model CompetencyLevel { + id Int @id @default(autoincrement()) + name String @unique + description String? + levelNumber Int + // Level Level @relation(fields: [levelNumber], references: [levelNumber]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + competencies CompetencyToCompetencyLevel[] + + @@map("competency_levels") +} + +model CompetencyToCompetencyLevel { + competency Competency @relation(fields: [competencyId], references: [id], onDelete: Cascade) + competencyId Int + competencyLevel CompetencyLevel @relation(fields: [competencyLevelId], references: [id], onDelete: Cascade) + competencyLevelId Int + + @@id([competencyId, competencyLevelId]) +} + +enum UserRolesEnum { + ADMIN + CONSUMER +} diff --git a/prisma/scripts/createViewQueries.ts b/prisma/scripts/createViewQueries.ts new file mode 100644 index 0000000..6ae1ccb --- /dev/null +++ b/prisma/scripts/createViewQueries.ts @@ -0,0 +1,90 @@ +export const createViewQueries = [ + ` + DROP VIEW IF EXISTS telemetry_user_details + `, + + ` + DROP VIEW IF EXISTS position_competency_mapping + `, + + ` + DROP VIEW IF EXISTS metrics_wpcas_overall + `, + + ` + CREATE VIEW telemetry_user_details AS + SELECT + u."userId", + u."userName", + u."isAdmin", + u.designation AS position, + COUNT(rt_surveyed.id) FILTER (WHERE rt_surveyed.status = 'COMPLETED') AS "surveyedCount", + COUNT(rt_filled.id) FILTER (WHERE rt_filled.status = 'COMPLETED') AS "surveysFilled", + COUNT(rt_received.id) AS "surveysReceived", + COUNT(rt_pending.id) FILTER (WHERE rt_pending.status = 'PENDING') AS "surveysYetToBeFilled", + COALESCE(NULLIF(COUNT(rt_filled.id) FILTER (WHERE rt_filled.status = 'COMPLETED'), 0) / NULLIF(COUNT(rt_received.id), 0), 0) AS "surveyResponseRate", + COALESCE( + ( + SELECT ss.score + FROM survey_scores ss + JOIN survey_form sf ON ss."surveyFormId" = sf.id + WHERE sf."userId" = u."userId" + ORDER BY sf."createdAt" DESC + LIMIT 1 + ), null + ) AS "wpcasScore" + FROM + "UserMetadata" u + LEFT JOIN + response_tracker rt_surveyed ON rt_surveyed."assesseeId" = u."userId" + LEFT JOIN + response_tracker rt_filled ON rt_filled."assessorId" = u."userId" + LEFT JOIN + response_tracker rt_received ON rt_received."assesseeId" = u."userId" + LEFT JOIN + response_tracker rt_pending ON rt_pending."assessorId" = u."userId" + GROUP BY + u."userId", u."userName", u."designation", u."isAdmin" + `, + + ` + CREATE VIEW position_competency_mapping AS + SELECT + d.id AS "designationId", + d.name AS "designationName", + r.id AS "roleId", + r.name AS "roleName", + c.id AS "competencyId", + c.name AS "competencyName", + cl.id AS "competencyLevelId", + cl.name AS "competencyLevelName", + cl."levelNumber" AS "competencyLevelNumber" + FROM + designations d + JOIN + "DesignationToRole" dr ON d.id = dr."designationId" + JOIN + roles r ON dr."roleId" = r.id + JOIN + "RoleToCompetency" rc ON r.id = rc."roleId" + JOIN + competencies c ON rc."competencyId" = c.id + JOIN + "CompetencyToCompetencyLevel" ccl ON c.id = ccl."competencyId" + JOIN + competency_levels cl ON ccl."competencyLevelId" = cl.id + ORDER BY + d.id, r.id, c.id, cl.id, cl."levelNumber" + `, + + ` + CREATE VIEW metrics_wpcas_overall AS + SELECT + (SELECT COUNT(*) FROM "UserMetadata") AS "registeredUsers", + (SELECT COUNT(*) FROM response_tracker) AS "overallSurveysReceived", + (SELECT COUNT(*) FROM response_tracker WHERE status = 'COMPLETED') AS "overallSurveysCompleted", + (SELECT COUNT(*) FROM response_tracker WHERE status = 'PENDING') AS "unfilledSurveyCount", + COALESCE(NULLIF((SELECT COUNT(*) FROM response_tracker WHERE status = 'PENDING'), 0) / NULLIF((SELECT COUNT(*) FROM response_tracker), 0), 0) AS "overallSurveyNonResponseRate", + COALESCE(NULLIF((SELECT COUNT(*) FROM response_tracker WHERE status = 'COMPLETED'), 0) / NULLIF((SELECT COUNT(*) FROM response_tracker), 0), 0) AS "overallSurveyResponseRate" + ` +]; \ No newline at end of file diff --git a/prisma/scripts/moveViewsQueries.ts b/prisma/scripts/moveViewsQueries.ts new file mode 100644 index 0000000..e91b178 --- /dev/null +++ b/prisma/scripts/moveViewsQueries.ts @@ -0,0 +1,81 @@ +const dbName = process.env.DATABASE_NAME; +const dbUserName = process.env.DATABASE_USERNAME; +const dbPassword = process.env.DATABASE_PASSWORD; +const dbPort = process.env.DATABASE_PORT; +const dbHost = '172.17.0.1'; + +export const copyViewQueries = [ + `CREATE EXTENSION IF NOT EXISTS postgres_fdw`, + ` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_foreign_server + WHERE srvname = 'wpcas_server' + ) THEN + EXECUTE 'CREATE SERVER wpcas_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (host ''${dbHost}'', dbname ''${dbName}'', port ''${dbPort}'')'; + END IF; + END $$; + `, + ` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_user_mappings + WHERE srvname = 'wpcas_server' AND usename = '${dbUserName}' + ) THEN + EXECUTE 'CREATE USER MAPPING FOR ${dbUserName} + SERVER wpcas_server + OPTIONS (user ''${dbUserName}'', password ''${dbPassword}'')'; + END IF; + END $$; + `, + ` + CREATE FOREIGN TABLE IF NOT EXISTS telemetry_user_details ( + "userId" TEXT, + "userName" TEXT, + "isAdmin" BOOLEAN, + "position" TEXT, + "surveyedCount" BIGINT, + "surveysFilled" BIGINT, + "surveysReceived" BIGINT, + "surveysYetToBeFilled" BIGINT, + "surveyResponseRate" BIGINT, + "wpcasScore" DOUBLE PRECISION + ) + SERVER wpcas_server + OPTIONS (schema_name 'public', table_name 'telemetry_user_details'); + `, + ` + CREATE FOREIGN TABLE IF NOT EXISTS position_competency_mapping ( + "designationId" INTEGER, + "designationName" TEXT, + "roleId" INTEGER, + "roleName" TEXT, + "competencyId" INTEGER, + "competencyName" TEXT, + "competencyLevelId" INTEGER, + "competencyLevelName" TEXT, + "competencyLevelNumber" INTEGER + ) + SERVER wpcas_server + OPTIONS (schema_name 'public', table_name 'position_competency_mapping'); + `, + ` + CREATE FOREIGN TABLE IF NOT EXISTS metrics_wpcas_overall ( + "registeredUsers" BIGINT, + "overallSurveysReceived" BIGINT, + "overallSurveysCompleted" BIGINT, + "unfilledSurveyCount" BIGINT, + "overallSurveyNonResponseRate" BIGINT, + "overallSurveyResponseRate" BIGINT, + "timeStamp" date + ) + SERVER wpcas_server + OPTIONS (schema_name 'public', table_name 'metrics_wpcas_overall'); + ` +]; \ No newline at end of file diff --git a/prisma/seeds/data/mock-competencies.data.ts b/prisma/seeds/data/mock-competencies.data.ts new file mode 100644 index 0000000..4599204 --- /dev/null +++ b/prisma/seeds/data/mock-competencies.data.ts @@ -0,0 +1,62 @@ +export const competencies = [ + { + id: 1, + name: "Strategic Planning", + description: + "Ability to formulate and execute long-term plans aligned with the organization's goals.", + }, + { + id: 2, + name: "Data Analysis", + description: + "Proficiency in analyzing data and market trends to inform strategic decisions.", + }, + { + id: 3, + name: "Innovation Management", + description: + "Skill in fostering innovation and adapting to technology advancements.", + }, + { + id: 4, + name: "Change Leadership", + description: + "Capacity to drive and manage organizational change in a dynamic IT environment.", + }, + { + id: 5, + name: "Project Management", + description: + "Effective planning, execution, and supervision of projects to achieve desired outcomes.", + }, + { + id: 6, + name: "Team Collaboration", + description: + "Collaborative work with cross-functional teams to deliver projects efficiently.", + }, + { + id: 7, + name: "Technical Expertise", + description: + "In-depth technical knowledge and proficiency in relevant tools and technologies.", + }, + { + id: 8, + name: "Quality Assurance", + description: + "Ensuring the quality of software products through comprehensive testing and validation.", + }, + { + id: 9, + name: "Problem Solving", + description: + "Identifying and resolving complex technical challenges and issues.", + }, + { + id: 10, + name: "Strategic Leadership", + description: + "Leading and directing organizational strategy to achieve long-term goals and success.", + }, +]; diff --git a/prisma/seeds/data/mock-competenciesToCompetencyLevels.data.ts b/prisma/seeds/data/mock-competenciesToCompetencyLevels.data.ts new file mode 100644 index 0000000..1b5f974 --- /dev/null +++ b/prisma/seeds/data/mock-competenciesToCompetencyLevels.data.ts @@ -0,0 +1,165 @@ +export const competenciesToCompetencyLevels = [ + // Strategic Planning + { + competencyId: 1, // Associated with "Strategic Planning" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 1, // Associated with "Strategic Planning" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 1, // Associated with "Strategic Planning" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + { + competencyId: 1, // Associated with "Strategic Planning" + competencyLevelId: 4, // Associated with "Strategic Leadership" + }, + { + competencyId: 1, // Associated with "Strategic Planning" + competencyLevelId: 5, // Associated with "Industry Thought Leader" + }, + + // Data Analysis + { + competencyId: 2, // Associated with "Data Analysis" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 2, // Associated with "Data Analysis" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 2, // Associated with "Data Analysis" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + + // Innovation Management + { + competencyId: 3, // Associated with "Innovation Management" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 3, // Associated with "Innovation Management" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + + // Change Leadership + { + competencyId: 4, // Associated with "Change Leadership" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 4, // Associated with "Change Leadership" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 4, // Associated with "Change Leadership" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + { + competencyId: 4, // Associated with "Change Leadership" + competencyLevelId: 4, // Associated with "Strategic Leadership" + }, + + // Project Management + { + competencyId: 5, // Associated with "Project Management" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 5, // Associated with "Project Management" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 5, // Associated with "Project Management" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + { + competencyId: 5, // Associated with "Project Management" + competencyLevelId: 4, // Associated with "Strategic Leadership" + }, + { + competencyId: 5, // Associated with "Project Management" + competencyLevelId: 5, // Associated with "Industry Thought Leader" + }, + + // Team Collaboration + { + competencyId: 6, // Associated with "Team Collaboration" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 6, // Associated with "Team Collaboration" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 6, // Associated with "Team Collaboration" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + + // Technical Expertise + { + competencyId: 7, // Associated with "Technical Expertise" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 7, // Associated with "Technical Expertise" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 7, // Associated with "Technical Expertise" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + + // Quality Assurance + { + competencyId: 8, // Associated with "Quality Assurance" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 8, // Associated with "Quality Assurance" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 8, // Associated with "Quality Assurance" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + + // Problem Solving + { + competencyId: 9, // Associated with "Problem Solving" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 9, // Associated with "Problem Solving" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 9, // Associated with "Problem Solving" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + + // Strategic Leadership + { + competencyId: 10, // Associated with "Strategic Leadership" + competencyLevelId: 1, // Associated with "Basic Proficiency" + }, + { + competencyId: 10, // Associated with "Strategic Leadership" + competencyLevelId: 2, // Associated with "Intermediate Skill" + }, + { + competencyId: 10, // Associated with "Strategic Leadership" + competencyLevelId: 3, // Associated with "Advanced Expertise" + }, + { + competencyId: 10, // Associated with "Strategic Leadership" + competencyLevelId: 4, // Associated with "Strategic Leadership" + }, + { + competencyId: 10, // Associated with "Strategic Leadership" + competencyLevelId: 5, // Associated with "Industry Thought Leader" + }, +]; diff --git a/prisma/seeds/data/mock-competencyLevels.data.ts b/prisma/seeds/data/mock-competencyLevels.data.ts new file mode 100644 index 0000000..d56bb01 --- /dev/null +++ b/prisma/seeds/data/mock-competencyLevels.data.ts @@ -0,0 +1,27 @@ +export const competencyLevels = [ + { + name: "Basic Proficiency", + description: "Competency Level 1", + levelNumber: 1, // Associated with Level 1 + }, + { + name: "Intermediate Skill", + description: "Competency Level 2", + levelNumber: 2, // Associated with Level 2 + }, + { + name: "Advanced Expertise", + description: "Competency Level 3", + levelNumber: 3, // Associated with Level 3 + }, + { + name: "Strategic Leadership", + description: "Competency Level 4", + levelNumber: 4, // Associated with Level 4 + }, + { + name: "Industry Thought Leader", + description: "Competency Level 5", + levelNumber: 5, // Associated with Level 5 + }, +]; diff --git a/prisma/seeds/data/mock-designation.data.ts b/prisma/seeds/data/mock-designation.data.ts new file mode 100644 index 0000000..e4e71e2 --- /dev/null +++ b/prisma/seeds/data/mock-designation.data.ts @@ -0,0 +1,47 @@ +export const designations = [ + { + id: 1, + name: "CEO", + description: "Chief Executive Officer", + }, + { + id: 2, + name: "CTO", + description: "Chief Technology Officer", + }, + { + id: 3, + name: "CIO", + description: "Chief Information Officer", + }, + { + id: 4, + name: "VP of Engineering", + description: "Vice President of Engineering", + }, + { + id: 5, + name: "Software Architect", + description: "Senior Software Architect", + }, + { + id: 6, + name: "Software Engineer", + description: "Software Development Engineer", + }, + { + id: 7, + name: "QA Engineer", + description: "Quality Assurance Engineer", + }, + { + id: 8, + name: "Systems Administrator", + description: "IT Systems Administrator", + }, + { + id: 9, + name: "Network Administrator", + description: "Network Systems Administrator", + } +]; diff --git a/prisma/seeds/data/mock-roles.data.ts b/prisma/seeds/data/mock-roles.data.ts new file mode 100644 index 0000000..6d16513 --- /dev/null +++ b/prisma/seeds/data/mock-roles.data.ts @@ -0,0 +1,223 @@ +export const roles = [ + // For CEO + { + id: 1, + name: "Corporate Strategy", + description: + "Developing and overseeing the organization's strategic plan to achieve its objectives.", + designationId: 1, // CEO + }, + { + id: 2, + name: "Financial Management", + description: + "Managing the company's finances, including budgeting, financial planning, and financial risk management.", + designationId: 1, // CEO + }, + { + id: 3, + name: "Talent Management", + description: + "Attracting, retaining, and developing top talent within the organization.", + designationId: 1, // CEO + }, + + // For CTO + { + id: 4, + name: "Technology Visionary", + description: + "The CTO defines the organization's long-term technology strategy, ensuring alignment with business goals and emerging industry trends.", + designationId: 2, // CTO + }, + { + id: 5, + name: "Innovation Leader", + description: + "Responsible for fostering innovation, the CTO drives research and development efforts to create cutting-edge solutions and stay competitive.", + designationId: 2, // CTO + }, + { + id: 6, + name: "Technical Advisor", + description: + "Provides technical expertise to guide decision-making, assess risks, and choose the right technologies for projects.", + designationId: 2, // CTO + }, + { + id: 7, + name: "Team Builder", + description: + "Recruits and manages top technical talent, fostering a culture of excellence and professional growth within the organization.", + designationId: 2, // CTO + }, + + // CIO + { + id: 8, + name: "Cybersecurity Oversight", + description: + "The CIO plays a critical role in safeguarding the organization's digital assets, implementing security measures, and mitigating cybersecurity threats.", + designationId: 3, // CIO + }, + { + id: 9, + name: "IT Governance", + description: + "The CIO establishes IT governance frameworks, policies, and procedures to ensure efficient and compliant IT operations within the organization.", + designationId: 3, // CIO + }, + { + id: 10, + name: "Resource Management", + description: + "CIOs manage IT budgets, resources, and teams, optimizing technology investments and ensuring IT projects are delivered on time and within budget.", + designationId: 3, // CIO + }, + + // VP of Engineering + { + id: 11, + name: "Team Leadership", + description: "Managing and mentoring engineering teams.", + designationId: 4, // VP of Engineering + }, + { + id: 12, + name: "Project Management", + description: "Overseeing project execution and delivery.", + designationId: 4, // VP of Engineering + }, + { + id: 13, + name: "Resource Allocation", + description: "Efficiently allocating resources and budgets.", + designationId: 4, // VP of Engineering + }, + { + id: 14, + name: "Innovation", + description: + "Promoting innovation and staying current with technology trends.", + designationId: 4, // VP of Engineering + }, + + // Software Architect + { + id: 15, + name: "System Design", + description: "Creating high-level system architecture and design.", + designationId: 5, // Software Architect + }, + { + id: 16, + name: "Technology Selection", + description: "Choosing the right tech stack for projects.", + designationId: 5, // Software Architect + }, + { + id: 17, + name: "Quality Assurance", + description: "Ensuring software meets quality standards.", + designationId: 5, // Software Architect + }, + { + id: 18, + name: "Performance Optimization", + description: "Improving system performance.", + designationId: 5, // Software Architect + }, + { + id: 19, + name: "Team Guidance", + description: "Providing technical leadership and mentoring.", + designationId: 5, // Software Architect + }, + + // Software Engineer + { + id: 20, + name: "Software Development", + description: "Writing, testing, and maintaining software applications.", + designationId: 6, // Software Engineer + }, + { + id: 21, + name: "Problem Solving", + description: "Identifying and resolving technical challenges.", + designationId: 6, // Software Engineer + }, + { + id: 22, + name: "Collaboration", + description: "Working with cross-functional teams to deliver projects.", + designationId: 6, // Software Engineer + }, + + // QA Engineer + { + id: 23, + name: "Test Planning and Strategy", + description: + "Creating test plans and strategies to ensure comprehensive testing of software.", + designationId: 7, // QA Engineer + }, + { + id: 24, + name: "Test Execution", + description: + "Performing testing, identifying defects, and ensuring software quality.", + designationId: 7, // QA Engineer + }, + { + id: 25, + name: "Automation Testing", + description: + "Developing and maintaining automated test scripts to improve efficiency and coverage.", + designationId: 7, // QA Engineer + }, + + // Systems Administrator + { + id: 26, + name: "System Maintenance", + description: + "Managing and maintaining IT infrastructure to ensure it runs smoothly.", + designationId: 8, // Systems Administrator + }, + { + id: 27, + name: "System Security Management", + description: + "Protecting systems and data from threats and vulnerabilities.", + designationId: 8, // Systems Administrator + }, + { + id: 28, + name: "User Support", + description: + "Providing technical assistance and troubleshooting for employees.", + designationId: 8, // Systems Administrator + }, + + // Network Administrator + { + id: 29, + name: "Network Configuration and Maintenance", + description: "Setting up and maintaining network infrastructure", + designationId: 9, // Network Administrator + }, + { + id: 30, + name: "Security Management", + description: + "Protecting the network from cyber threats and vulnerabilities", + designationId: 9, // Network Administrator + }, + { + id: 31, + name: "Network Performance Optimization", + description: "Ensuring network efficiency and reliability", + designationId: 9, // Network Administrator + }, +]; diff --git a/prisma/seeds/data/mock-rolesToCompetencies.data.ts b/prisma/seeds/data/mock-rolesToCompetencies.data.ts new file mode 100644 index 0000000..1393d82 --- /dev/null +++ b/prisma/seeds/data/mock-rolesToCompetencies.data.ts @@ -0,0 +1,302 @@ +export const rolesToCompetencies = [ + // For CEO - Corporate Strategy + { + roleId: 1, + competencyId: 1, // Strategic Planning + }, + { + roleId: 1, + competencyId: 3, // Innovation Management + }, + // For CEO - Financial Management + { + roleId: 2, + competencyId: 10, // Strategic Leadership + }, + { + roleId: 2, + competencyId: 3, // Innovation Management + }, + // For CEO - Talent Management + { + roleId: 3, + competencyId: 6, // Team Collaboration + }, + { + roleId: 3, + competencyId: 10, // Strategic Leadership + }, + + // For CTO - Technology Visionary + { + roleId: 4, + competencyId: 1, // Strategic Planning + }, + { + roleId: 4, + competencyId: 3, // Innovation Management + }, + { + roleId: 4, + competencyId: 7, // Technical Expertise + }, + { + roleId: 4, + competencyId: 6, // Team Collaboration + }, + // For CTO - Innovation Leader + { + roleId: 5, + competencyId: 1, // Strategic Planning + }, + { + roleId: 5, + competencyId: 3, // Innovation Management + }, + { + roleId: 5, + competencyId: 7, // Technical Expertise + }, + { + roleId: 5, + competencyId: 6, // Team Collaboration + }, + // For CTO - Technical Advisor + { + roleId: 6, + competencyId: 1, // Strategic Planning + }, + { + roleId: 6, + competencyId: 7, // Technical Expertise + }, + { + roleId: 6, + competencyId: 9, // Problem Solving + }, + // For CTO - Team Builder + { + roleId: 7, + competencyId: 6, // Team Collaboration + }, + { + roleId: 7, + competencyId: 10, // Strategic Leadership + }, + // For CIO - Cybersecurity Oversight + { + roleId: 8, + competencyId: 4, // Change Leadership + }, + { + roleId: 8, + competencyId: 2, // Data Analysis + }, + // For CIO - IT Governance + { + roleId: 9, + competencyId: 1, // Strategic Planning + }, + { + roleId: 9, + competencyId: 7, // Technical Expertise + }, + // For CIO - Resource Management + { + roleId: 10, + competencyId: 10, // Strategic Leadership + }, + { + roleId: 10, + competencyId: 7, // Technical Expertise + }, + // For VP of Engineering - Team Leadership + { + roleId: 11, + competencyId: 6, // Team Collaboration + }, + { + roleId: 11, + competencyId: 10, // Strategic Leadership + }, + // For VP of Engineering - Project Management + { + roleId: 12, + competencyId: 10, // Strategic Leadership + }, + { + roleId: 12, + competencyId: 6, // Team Collaboration + }, + // For VP of Engineering - Resource Allocation + { + roleId: 13, + competencyId: 10, // Strategic Leadership + }, + { + roleId: 13, + competencyId: 7, // Technical Expertise + }, + // For VP of Engineering - Innovation + { + roleId: 14, + competencyId: 3, // Innovation Management + }, + { + roleId: 14, + competencyId: 6, // Team Collaboration + }, + // For Software Architect - System Design + { + roleId: 15, + competencyId: 10, // Strategic Leadership + }, + { + roleId: 15, + competencyId: 7, // Technical Expertise + }, + // For Software Architect - Technology Selection + { + roleId: 16, + competencyId: 7, // Technical Expertise + }, + { + roleId: 16, + competencyId: 6, // Team Collaboration + }, + // For Software Architect - Quality Assurance + { + roleId: 17, + competencyId: 8, // Quality Assurance + }, + { + roleId: 17, + competencyId: 9, // Problem Solving + }, + // For Software Architect - Performance Optimization + { + roleId: 18, + competencyId: 4, // Change Leadership + }, + { + roleId: 18, + competencyId: 7, // Technical Expertise + }, + // For Software Architect - Team Guidance + { + roleId: 19, + competencyId: 6, // Team Collaboration + }, + { + roleId: 19, + competencyId: 7, // Technical Expertise + }, + // For Software Engineer - Software Development + { + roleId: 20, + competencyId: 6, // Team Collaboration + }, + { + roleId: 20, + competencyId: 9, // Problem Solving + }, + // For Software Engineer - Problem Solving + { + roleId: 21, + competencyId: 9, // Problem Solving + }, + { + roleId: 21, + competencyId: 6, // Team Collaboration + }, + // For Software Engineer - Collaboration + { + roleId: 22, + competencyId: 6, // Team Collaboration + }, + { + roleId: 22, + competencyId: 10, // Strategic Leadership + }, + // For QA Engineer - Test Planning and Strategy + { + roleId: 23, + competencyId: 1, // Strategic Planning + }, + { + roleId: 23, + competencyId: 8, // Quality Assurance + }, + // For QA Engineer - Test Execution + { + roleId: 24, + competencyId: 8, // Quality Assurance + }, + { + roleId: 24, + competencyId: 9, // Problem Solving + }, + // For QA Engineer - Automation Testing + { + roleId: 25, + competencyId: 8, // Quality Assurance + }, + { + roleId: 25, + competencyId: 9, // Problem Solving + }, + // For Systems Administrator - System Maintenance + { + roleId: 26, + competencyId: 9, // Problem Solving + }, + { + roleId: 26, + competencyId: 10, // Strategic Leadership + }, + // For Systems Administrator - System Security Management + { + roleId: 27, + competencyId: 4, // Change Leadership + }, + { + roleId: 27, + competencyId: 8, // Quality Assurance + }, + // For Systems Administrator - User Support + { + roleId: 28, + competencyId: 9, // Problem Solving + }, + { + roleId: 28, + competencyId: 6, // Team Collaboration + }, + // For Network Administrator - Network Configuration and Maintenance + { + roleId: 29, + competencyId: 1, // Strategic Planning + }, + { + roleId: 29, + competencyId: 7, // Technical Expertise + }, + // For Network Administrator - Security Management + { + roleId: 30, + competencyId: 4, // Change Leadership + }, + { + roleId: 30, + competencyId: 8, // Quality Assurance + }, + // For Network Administrator - Network Performance Optimization + { + roleId: 31, + competencyId: 4, // Change Leadership + }, + { + roleId: 31, + competencyId: 7, // Technical Expertise + }, +]; diff --git a/prisma/seeds/data/mock-users.data.ts b/prisma/seeds/data/mock-users.data.ts new file mode 100644 index 0000000..9df3807 --- /dev/null +++ b/prisma/seeds/data/mock-users.data.ts @@ -0,0 +1,164 @@ +import { UserRolesEnum } from "@prisma/client"; + +export const users = [ + { + id: "d71f8744-52b0-4ec3-9e7e-1f1266059821", // Unique UUID for Amit Sharma + email: "amit.sharma@example.com", + role: UserRolesEnum.ADMIN, + userName: "Amit Sharma", + password: "P@ssw0rd1", + designation: "CEO", + }, + { + id: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", // Unique UUID for Neha Patil + email: "neha.patil@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Neha Patil", + password: "P@ssw0rd2", + designation: "Software Engineer", + }, + { + id: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", // Unique UUID for Rajesh Kumar + email: "rajesh.kumar@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Rajesh Kumar", + password: "P@ssw0rd3", + designation: "Software Engineer", + }, + { + id: "0f5d0b13-8d72-46c9-a7c4-c1f7e5aa1f17", // Unique UUID for Priya Jain + email: "priya.jain@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Priya Jain", + password: "P@ssw0rd4", + designation: "QA Engineer", + }, + { + id: "bbf1f7cf-4216-458e-8d98-0d9204ae57ef", // Unique UUID for Arjun Singh + email: "arjun.singh@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Arjun Singh", + password: "P@ssw0rd5", + designation: "QA Engineer", + }, + { + id: "a3bf8f0e-22a9-4a5e-8c76-e80b67a1a6e9", // Unique UUID for Deepa Mishra + email: "deepa.mishra@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Deepa Mishra", + password: "P@ssw0rd6", + designation: "Software Architect", + }, + { + id: "ac6b9e42-efea-401a-bd67-9cf9110ac3c9", // Unique UUID for Rahul Verma + email: "rahul.verma@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Rahul Verma", + password: "P@ssw0rd7", + designation: "Software Architect", + }, + { + id: "0f82ac61-9801-488f-babb-3c2d23a413d0", // Unique UUID for Anita Sen + email: "anita.sen@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Anita Sen", + password: "P@ssw0rd8", + designation: "Software Engineer", + }, + { + id: "69d7871b-3b09-4b5b-9ea0-0ca27c2ac9e3", // Unique UUID for Mohit Gupta + email: "mohit.gupta@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Mohit Gupta", + password: "P@ssw0rd9", + designation: "Software Engineer", + }, + { + id: "7b8005e4-e3d5-4a0d-81d6-d2965a9d5c2c", // Unique UUID for Pooja Rawat + email: "pooja.rawat@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Pooja Rawat", + password: "P@ssw0rd10", + designation: "Software Engineer", + }, + { + id: "1536c7ad-234e-44d3-b6d4-8ca655b46e2a", // Unique UUID for Anurag Sharma + email: "anurag.sharma@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Anurag Sharma", + password: "P@ssw0rd11", + designation: "Software Engineer", + }, + { + id: "9a1f8f5a-0d9e-40e2-b7ea-df493a303db7", // Unique UUID for Meera Rani + email: "meera.rani@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Meera Rani", + password: "P@ssw0rd12", + designation: "Software Engineer", + }, + { + id: "fbd6d6b9-4c4f-48c1-9147-3006a369e190", // Unique UUID for Vikas Patel + email: "vikas.patel@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Vikas Patel", + password: "P@ssw0rd13", + designation: "Software Engineer", + }, + { + id: "4f67ae5a-c2a2-4652-81cc-4d1471e1a158", // Unique UUID for Divya Malik + email: "divya.malik@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Divya Malik", + password: "P@ssw0rd14", + designation: "Software Engineer", + }, + { + id: "a6a862ac-f5f7-4d6b-a297-c1497e5652e5", // Unique UUID for Rajiv Khanna + email: "rajiv.khanna@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Rajiv Khanna", + password: "P@ssw0rd15", + designation: "Software Engineer", + }, + { + id: "72e9272e-3a4e-45f6-877c-6567f3f9785a", // Unique UUID for Swati Shah + email: "swati.shah@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Swati Shah", + password: "P@ssw0rd16", + designation: "QA Engineer", + }, + { + id: "5d0b5fb7-d350-4315-988e-df878e96287d", // Unique UUID for Manish Agrawal + email: "manish.agrawal@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Manish Agrawal", + password: "P@ssw0rd17", + designation: "QA Engineer", + }, + { + id: "a1f3e09a-369f-4e7b-997b-891274f1020e", // Unique UUID for Pallavi Reddy + email: "pallavi.reddy@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Pallavi Reddy", + password: "P@ssw0rd18", + designation: "QA Engineer", + }, + { + id: "d35d1e80-d2df-4b6a-9a67-8ef5f2877a57", // Unique UUID for Deepak Mishra + email: "deepak.mishra@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Deepak Mishra", + password: "P@ssw0rd19", + designation: "QA Engineer", + }, + { + id: "7646a865-513f-4529-8ecf-170220e2d6b8", // Unique UUID for Komal Choudhary + email: "komal.choudhary@example.com", + role: UserRolesEnum.CONSUMER, + userName: "Komal Choudhary", + password: "P@ssw0rd20", + designation: "QA Engineer", + }, +]; diff --git a/prisma/seeds/seed.ts b/prisma/seeds/seed.ts new file mode 100644 index 0000000..bed1602 --- /dev/null +++ b/prisma/seeds/seed.ts @@ -0,0 +1,111 @@ +import { Prisma, PrismaClient } from "@prisma/client"; +import { competencies } from "./data/mock-competencies.data"; +import { competenciesToCompetencyLevels } from "./data/mock-competenciesToCompetencyLevels.data"; +import { competencyLevels } from "./data/mock-competencyLevels.data"; +import { designations } from "./data/mock-designation.data"; +import { roles } from "./data/mock-roles.data"; +import { rolesToCompetencies } from "./data/mock-rolesToCompetencies.data"; +import { users } from "./data/mock-users.data"; +import { Logger } from "@nestjs/common"; +import { copyViewQueries } from "../scripts/moveViewsQueries"; +import { createViewQueries } from "../scripts/createViewQueries"; +const { Client } = require('pg'); + +const prisma = new PrismaClient(); +const telemetryDbName = process.env.TELEMETRY_DATABASE_NAME; + +async function seed() { + await prisma.$transaction([ + prisma.designation.createMany({ + data: designations, + }), + + prisma.role.createMany({ + data: roles, + }), + + prisma.competencyLevel.createMany({ + data: competencyLevels, + }), + + prisma.competency.createMany({ + data: competencies, + }), + + prisma.competencyToCompetencyLevel.createMany({ + data: competenciesToCompetencyLevels, + }), + + prisma.roleToCompetency.createMany({ + data: rolesToCompetencies, + }), + + prisma.user.createMany({ + data: users, + }), + ]); +} + +async function createViews() { + let logger = new Logger("CreatingViews"); + logger.log(`Started creating views`); + + for (const sql of createViewQueries) { + logger.log(sql); + await prisma.$executeRaw`${Prisma.raw(sql)}`; + } + + const res:any = await prisma.$queryRaw`${Prisma.raw(`SELECT datname FROM pg_database WHERE datname = '${telemetryDbName}'`)}`; + if (res.length === 0) { + // Create the telemetry-views database if it does not exist + await prisma.$queryRaw`${Prisma.raw(`CREATE DATABASE "${telemetryDbName}"`)}`; + logger.log(`Database "${telemetryDbName}" created.`); + } else { + logger.log(`Database "${telemetryDbName}" already exists.`); + } + + logger.log(`Successfully created views`); +} + +async function moveViews() { + let logger = new Logger("MovingViews"); + + const telemetryClient = new Client({ + user: process.env.DATABASE_USERNAME, + host: '172.17.0.1', + database: process.env.TELEMETRY_DATABASE_NAME, + password: process.env.DATABASE_PASSWORD, + port: 5432, + }); + + await telemetryClient.connect(); + + logger.log(copyViewQueries); + + logger.log(`Started moving views`); + + for (const sql of copyViewQueries) { + logger.log(sql); + await telemetryClient.query(sql); + } + + await telemetryClient.end(); + + logger.log(`Successfully moved views`); +} + +// execute the functions +async function main() { + try { + // await seed(); + await createViews(); + await moveViews(); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +main(); diff --git a/src/admin-competency/admin-competency.controller.spec.ts b/src/admin-competency/admin-competency.controller.spec.ts new file mode 100644 index 0000000..59113dd --- /dev/null +++ b/src/admin-competency/admin-competency.controller.spec.ts @@ -0,0 +1,156 @@ +import * as pactum from "pactum"; +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import { CreateAdminCompetencyDto } from "./dto/create-admin-competency.dto"; +import { UpdateAdminCompetencyDto } from "./dto"; +import { AdminCompetencyModule } from "./admin-competency.module"; + +describe("AdminCompetencyController e2e", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [AdminCompetencyModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + afterAll(async () => { + await app.close(); + }); + + describe("AdminCompetencyController, findAll()", () => { + it("should get all admin competency", async () => { + const response = await pactum + .spec() + .get("/admin-competency") + .expectStatus(200); + + const adminCompetencies = JSON.parse(JSON.stringify(response.body)); + expect(adminCompetencies.message).toEqual( + "Successfully fetched all admin-competency" + ); + expect(adminCompetencies.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("AdminCompetencyController, findAllCompetencyNames()", () => { + it("should get admin competency by names", async () => { + const response = await pactum + .spec() + .get("/admin-competency/names") + .expectStatus(200); + + const adminCompetencies = JSON.parse(JSON.stringify(response.body)); + expect(adminCompetencies.message).toEqual( + `Successfully fetched all admin-competency` + ); + expect(adminCompetencies.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("AdminCompetencyController findOne()", () => { + const testData = { + competencyId: 1, + }; + it("should findOne admin competency by competencyId", async () => { + const response = await pactum + .spec() + .get(`/admin-competency/${testData.competencyId}`) + .withPathParams({ + competencyId: testData.competencyId, + }) + .expectStatus(200); + + const adminCompetncy = JSON.parse(JSON.stringify(response.body)); + expect(adminCompetncy).not.toBeNull(); + expect(adminCompetncy.message).toEqual( + `Successfully fetched admin-competency with competency id #${testData.competencyId}` + ); + expect(adminCompetncy.data.competencyId).toEqual(testData.competencyId); + expect(adminCompetncy.data.name).toBeDefined(); + expect(adminCompetncy.data.description).toBeDefined(); + }); + }); + + describe("AdminCompetencyController update()", () => { + it("should updated admin competency by competencyId", async () => { + const updatedAdminCompetencyDto: UpdateAdminCompetencyDto = { + name: "Updated Test Competency Name", + description: "Updated Test Description", + }; + const testData = { + competencyId: 1, + }; + const response = await pactum + .spec() + .patch(`/admin-competency/${testData.competencyId}`) + .withPathParams({ + competencyId: testData.competencyId, + }) + .withBody(updatedAdminCompetencyDto) + .expectStatus(200); + + const updatedAdminCompetency = JSON.parse(JSON.stringify(response.body)); + expect(updatedAdminCompetency).not.toBeNull(); + expect(updatedAdminCompetency.message).toEqual( + `Successfully updated admin-competency competency id #${testData.competencyId}` + ); + expect(updatedAdminCompetency.data.competencyId).toEqual( + testData.competencyId + ); + expect(updatedAdminCompetency.data.name).toBeDefined(); + expect(updatedAdminCompetency.data.description).toBeDefined(); + }); + }); + + describe("AdminCompetencyController remove()", () => { + it("should delete an admin competency by id", async () => { + const testData = { + competencyId: 5, + }; + const response = await pactum + .spec() + .delete(`/admin-competency/${testData.competencyId}`) + .withPathParams({ + competencyId: testData.competencyId, + }) + .expectStatus(200); + const deletedAdminCompetency = JSON.parse(JSON.stringify(response.body)); + console.log("deletedAdminCompetency", deletedAdminCompetency); + + expect(deletedAdminCompetency).not.toBeNull(); + expect(deletedAdminCompetency.message).toEqual( + `Successfully deleted admin-competency competency id #${testData.competencyId}` + ); + expect(deletedAdminCompetency.data).toHaveProperty("id"); + expect(deletedAdminCompetency.data.competencyId).toEqual( + testData.competencyId + ); + expect(deletedAdminCompetency.data.name).toBeDefined(); + expect(deletedAdminCompetency.data.competencyLevels).toBeDefined(); + expect(deletedAdminCompetency?.data.description).toBeDefined(); + expect(deletedAdminCompetency?.data.createdAt).toBeDefined(); + expect(deletedAdminCompetency?.data.updatedAt).toBeDefined(); + }); + }); +}); diff --git a/src/admin-competency/admin-competency.controller.ts b/src/admin-competency/admin-competency.controller.ts new file mode 100644 index 0000000..ed79a42 --- /dev/null +++ b/src/admin-competency/admin-competency.controller.ts @@ -0,0 +1,241 @@ +import { + Controller, + Delete, + Get, + HttpStatus, + Logger, + Param, + ParseIntPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { AdminCompetencyService } from "./admin-competency.service"; +import { + AdminCompetencyArrayResponseDto, + AdminCompetencyResponseDto, +} from "./dto"; + +@Controller("admin-competency") +@ApiTags("admin-competency") +export class AdminCompetencyController { + private readonly logger = new Logger(AdminCompetencyController.name); + + constructor( + private readonly adminCompetencyService: AdminCompetencyService + ) {} + + @Post("sync-competency-data") + @ApiOperation({ summary: "sync competency data" }) + @ApiResponse({ status: HttpStatus.CREATED, type: AdminCompetencyResponseDto }) + async syncCompetencyData(@Res() res): Promise { + try { + this.logger.log(`Initiated sync competency data`); + + const adminCompetency = + await this.adminCompetencyService.syncCompetencyData(); + + this.logger.log(`Successfully sync competency data`); + + return res.status(HttpStatus.CREATED).send({ + message: `Successfully sync competency data`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error("Failed to sync competency data", error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to sync competency data", + }); + } + } + + @Get() + @ApiOperation({ summary: "get all admin-competency" }) + @ApiResponse({ status: HttpStatus.OK, type: AdminCompetencyArrayResponseDto }) + async findAll(@Res() res): Promise { + try { + this.logger.log(`Initiated fetching all admin-competency`); + + const adminCompetency = await this.adminCompetencyService.findAll(); + + this.logger.log(`Successfully fetched all admin-competency`); + + return res.status(HttpStatus.OK).send({ + message: `Successfully fetched all admin-competency`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error("Failed to fetch all admin-competency", error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all admin-competency", + }); + } + } + + + @Get("/names") + @ApiOperation({ summary: "get all admin-competency" }) + @ApiResponse({ status: HttpStatus.OK, type: AdminCompetencyArrayResponseDto }) + async findAllCompetencyNames(@Res() res): Promise { + try { + this.logger.log(`Initiated fetching all admin-competency`); + + const adminCompetency = await this.adminCompetencyService.findAllCompetencyNames(); + + this.logger.log(`Successfully fetched all admin-competency`); + + return res.status(HttpStatus.OK).send({ + message: `Successfully fetched all admin-competency`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error("Failed to fetch all admin-competency", error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all admin-competency", + }); + } + } + + @Get(":competencyId") + @ApiOperation({ summary: "get admin-competency by competencyId" }) + @ApiResponse({ status: HttpStatus.OK, type: AdminCompetencyResponseDto }) + async findOne( + @Res() res, + @Param("competencyId", ParseIntPipe) competencyId: number + ): Promise { + try { + this.logger.log( + `Initiated fetching admin-competency with competency id #${competencyId}` + ); + + const adminCompetency = await this.adminCompetencyService.findOne( + competencyId + ); + + this.logger.log( + `Successfully fetched admin-competency with competency id #${competencyId}` + ); + + return res.status(HttpStatus.OK).send({ + message: `Successfully fetched admin-competency with competency id #${competencyId}`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error( + `Failed to fetch admin-competency with competency id #${competencyId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch admin-competency with competency id #${competencyId}`, + }); + } + } + + @Patch(":competencyId") + @ApiOperation({ summary: "update admin-competency" }) + @ApiResponse({ status: HttpStatus.OK, type: AdminCompetencyResponseDto }) + async update( + @Res() res, + @Param("competencyId", ParseIntPipe) competencyId: number + ): Promise { + try { + this.logger.log( + `Initiated updating admin-competency competency id #${competencyId}` + ); + + const adminCompetency = await this.adminCompetencyService.update( + competencyId + ); + + this.logger.log( + `Successfully updated admin-competency competency id #${competencyId}` + ); + + return res.status(HttpStatus.OK).send({ + message: `Successfully updated admin-competency competency id #${competencyId}`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error( + `Failed to update admin-competency competency id #${competencyId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to update admin-competency competency id #${competencyId}`, + }); + } + } + + @Delete(":competencyId") + @ApiOperation({ summary: "delete admin-competency" }) + @ApiResponse({ status: HttpStatus.OK, type: AdminCompetencyResponseDto }) + async remove( + @Res() res, + @Param("competencyId", ParseIntPipe) competencyId: number + ): Promise { + try { + this.logger.log( + `Initiated deleting admin-competency competency id #${competencyId}` + ); + + const adminCompetency = await this.adminCompetencyService.remove( + competencyId + ); + + this.logger.log( + `Successfully deleted admin-competency competency id #${competencyId}` + ); + + return res.status(HttpStatus.OK).send({ + message: `Successfully deleted admin-competency competency id #${competencyId}`, + data: adminCompetency, + }); + } catch (error) { + this.logger.error( + `Failed to delete admin-competency competency id #${competencyId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to delete admin-competency competency id #${competencyId}`, + }); + } + } +} diff --git a/src/admin-competency/admin-competency.module.ts b/src/admin-competency/admin-competency.module.ts new file mode 100644 index 0000000..02b392f --- /dev/null +++ b/src/admin-competency/admin-competency.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { AdminCompetencyController } from "./admin-competency.controller"; +import { AdminCompetencyService } from "./admin-competency.service"; +import { PrismaModule } from "../prisma/prisma.module"; +import { MockCompetencyService } from "../mockModules/mock-competency/mock-competency.service"; +import { MockCompetencyLevelService } from "../mockModules/mock-competency-level/mock-competency-level.service"; + +@Module({ + imports: [PrismaModule], + controllers: [AdminCompetencyController], + providers: [ + AdminCompetencyService, + MockCompetencyService, + MockCompetencyLevelService, + ], + exports: [AdminCompetencyService], +}) +export class AdminCompetencyModule {} diff --git a/src/admin-competency/admin-competency.service.spec.ts b/src/admin-competency/admin-competency.service.spec.ts new file mode 100644 index 0000000..47c246c --- /dev/null +++ b/src/admin-competency/admin-competency.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminCompetencyService } from './admin-competency.service'; + +describe('AdminCompetencyService', () => { + let service: AdminCompetencyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminCompetencyService], + }).compile(); + + service = module.get(AdminCompetencyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/admin-competency/admin-competency.service.ts b/src/admin-competency/admin-competency.service.ts new file mode 100644 index 0000000..04b73c4 --- /dev/null +++ b/src/admin-competency/admin-competency.service.ts @@ -0,0 +1,151 @@ +import { + Inject, + Injectable, + NotFoundException, + forwardRef, +} from "@nestjs/common"; +import _ from "lodash"; +import { MockCompetencyService } from "../mockModules/mock-competency/mock-competency.service"; +import { PrismaService } from "../prisma/prisma.service"; + +@Injectable() +export class AdminCompetencyService { + constructor( + private prisma: PrismaService, + @Inject(forwardRef(() => MockCompetencyService)) + private mockCompetencyService: MockCompetencyService + ) {} + + public async syncCompetencyData() { + const competenciesData = + await this.mockCompetencyService.findAllCompetenciesWithLevelNames(); + + const response = await Promise.all( + _.map(competenciesData, async (data) => { + const adminCompetencyPayload = this.createAdminCompetencyPayload(data); + + return await this.createOrUpdateQuery(adminCompetencyPayload); + }) + ); + + return response; + } + + public createAdminCompetencyPayload(competency) { + const { competencyLevels, description, id, name } = competency; + + const competencyLevelData = _.map( + competencyLevels, + (competencyLevelData) => { + const { levelNumber, name } = + competencyLevelData?.competencyLevel || {}; + + return { + competencyLevelNumber: levelNumber, + competencyLevelName: name, + }; + } + ); + + return { + id, + competencyLevels: JSON.parse(JSON.stringify(competencyLevelData)), + competencyId: id, + name, + description, + }; + } + + public async createOrUpdateQuery(adminCompetencyPayload) { + return this.prisma.adminCompetency.upsert({ + where: { + competencyId: adminCompetencyPayload.competencyId, + }, + update: adminCompetencyPayload, + create: adminCompetencyPayload, + }); + } + + public async findAll() { + const adminCompetency = await this.prisma.adminCompetency.findMany(); + return adminCompetency; + } + + public async findAllCompetencyNames() { + const adminCompetencies = await this.prisma.adminCompetency.findMany({ + select: { + competencyId: true, + name: true, + }, + }); + return adminCompetencies; + } + + public async findOne(competencyId: number) { + const adminCompetency = await this.prisma.adminCompetency.findUnique({ + where: { + competencyId, + }, + }); + + if (!adminCompetency) + throw new NotFoundException( + `Admin competency with competency id #${competencyId} not found ` + ); + return adminCompetency; + } + + public async update(competencyId: number) { + const adminCompetencyData = await this.prisma.adminCompetency.findUnique({ + where: { competencyId }, + }); + + const competencyData = await this.prisma.competency.findUnique({ + where: { id: competencyId }, + include: { + competencyLevels: { + include: { + competencyLevel: true, + }, + }, + }, + }); + + if (!adminCompetencyData && !competencyData) { + throw new Error( + `Competency not found with competency Id #${competencyId}` + ); + } + + if (adminCompetencyData && !competencyData) { + await this.prisma.adminCompetency.delete({ where: { competencyId } }); + return; + } + + if (competencyData) { + const adminCompetencyPayload = + this.createAdminCompetencyPayload(competencyData); + + return await this.createOrUpdateQuery(adminCompetencyPayload); + } + } + + public async remove(competencyId: number) { + const adminCompetency = await this.prisma.adminCompetency.delete({ + where: { + competencyId, + }, + }); + return adminCompetency; + } + + // public async fetchCompetencyLevel(competencyId:number, competencyLevelNumber: number){ + // try { + // const competency = await this.findOne(competencyId); + // const competencyLevel = _.get(competency, "competencyLevels", []); + // return _.find(competencyLevel, competencyLevelNumber); + // } catch (error) { + // throw error; + // } + // } +} diff --git a/src/admin-competency/dto/create-admin-competency.dto.ts b/src/admin-competency/dto/create-admin-competency.dto.ts new file mode 100644 index 0000000..ff475bd --- /dev/null +++ b/src/admin-competency/dto/create-admin-competency.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { + IsArray, + IsDate, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; + +export class CompetencyLevels { + @IsInt() + @IsNotEmpty() + competencyLevelNumber: number; + + @IsString() + @IsNotEmpty() + competencyLevelName: string; +} + +export class CreateAdminCompetencyDto { + @IsNotEmpty() + @IsInt() + competencyId: number; + + @IsNotEmpty() + @IsString() + name: string; + + @IsNotEmpty() + @IsString() + @IsOptional() + description?: string; + + @ApiProperty({ + type: CompetencyLevels, + isArray: true, + example: [{ competencyLevelNumber: 1, competencyLevelName: "Level-1" }], + }) + @IsNotEmpty() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CompetencyLevels) + competencyLevels: CompetencyLevels[]; + + @IsNotEmpty() + @IsDate() + @IsOptional() + createdAt?: Date; + + @IsNotEmpty() + @IsDate() + @IsOptional() + updatedAt?: Date; +} diff --git a/src/admin-competency/dto/index.ts b/src/admin-competency/dto/index.ts new file mode 100644 index 0000000..3e53146 --- /dev/null +++ b/src/admin-competency/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./create-admin-competency.dto"; +export * from "./response-admin-competency.dto"; +export * from "./update-admin-competency.dto"; diff --git a/src/admin-competency/dto/response-admin-competency.dto.ts b/src/admin-competency/dto/response-admin-competency.dto.ts new file mode 100644 index 0000000..7661623 --- /dev/null +++ b/src/admin-competency/dto/response-admin-competency.dto.ts @@ -0,0 +1,23 @@ +import { CompetencyLevels } from "./create-admin-competency.dto"; + +export class AdminCompetencyResponse { + id: number; + competencyId: number; + name: string; + description?: string; + competencyLevels: CompetencyLevels[]; + createdAt?: Date; + updatedAt?: Date; +} + +export class AdminCompetencyResponseDto { + data?: AdminCompetencyResponse; + message: string; + statusCode?: number; +} + +export class AdminCompetencyArrayResponseDto { + data?: AdminCompetencyResponse[]; + message: string; + statusCode?: number; +} diff --git a/src/admin-competency/dto/update-admin-competency.dto.ts b/src/admin-competency/dto/update-admin-competency.dto.ts new file mode 100644 index 0000000..97fa4ac --- /dev/null +++ b/src/admin-competency/dto/update-admin-competency.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAdminCompetencyDto } from './create-admin-competency.dto'; + +export class UpdateAdminCompetencyDto extends PartialType(CreateAdminCompetencyDto) {} diff --git a/src/app.module.ts b/src/app.module.ts index 9046544..1ffe9e7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,20 @@ import { AppController } from "./app.controller"; import { AppService } from "./app.service"; import { PrismaModule } from "./prisma/prisma.module"; import { PrismaService } from "./prisma/prisma.service"; +import { SurveyFormModule } from "./survey-form/survey-form.module"; +import { ResponseTrackerModule } from "./response-tracker/response-tracker.module"; +import { MockFracModule } from "./mockModules/mock.module"; +import { SurveyScoreModule } from "./survey-score/survey-score.module"; +import { QuestionBankModule } from "./question-bank/question-bank.module"; +import { SurveyConfigModule } from "./survey-config/survey-config.module"; +import { UserMetadataModule } from './user-metadata/user-metadata.module'; +import { AdminCompetencyModule } from "./admin-competency/admin-competency.module"; +import { ScheduledTasksModule } from './scheduled-tasks/scheduled-tasks.module'; +import { ScheduleModule } from "@nestjs/schedule"; +import { SurveyModule } from './survey/survey.module'; +import { FileUploadModule } from './file-upload/file-upload.module'; +import { ExternalServicesModule } from "./external-services/external-services.module"; +import { CredentialDIDModule } from "./credential-did/credential-did.module"; @Module({ imports: [ @@ -11,6 +25,20 @@ import { PrismaService } from "./prisma/prisma.service"; isGlobal: true, }), PrismaModule, + MockFracModule, + SurveyFormModule, + ResponseTrackerModule, + SurveyScoreModule, + QuestionBankModule, + SurveyConfigModule, + UserMetadataModule, + AdminCompetencyModule, + ScheduledTasksModule, + ScheduleModule.forRoot(), + SurveyModule, + FileUploadModule, + ExternalServicesModule, + CredentialDIDModule ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/src/config/multer-options.config.ts b/src/config/multer-options.config.ts new file mode 100644 index 0000000..442d537 --- /dev/null +++ b/src/config/multer-options.config.ts @@ -0,0 +1,31 @@ +import { UnsupportedMediaTypeException } from "@nestjs/common"; +import { MulterOptions } from "@nestjs/platform-express/multer/interfaces/multer-options.interface"; +import { diskStorage } from "multer"; +export const csvFileFilter = ( + req: Request, + file: Express.Multer.File, + callback +) => { + if (!file.originalname.match(/\.(csv)$/)) { + return callback( + new UnsupportedMediaTypeException("Only csv files are allowed!"), + false + ); + } + callback(null, true); +}; + +export const multerOptions: MulterOptions = { + dest: "src/uploads", // this is the destination of the uploaded files on disk + limits: { + fileSize: 1024 * 1024 * 5, // max size 5MB + }, + fileFilter: csvFileFilter, + storage: diskStorage({ + destination: "src/uploads", + filename: (_req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + cb(null, `${file.fieldname}-${uniqueSuffix}${file.originalname}`); + }, + }), +}; diff --git a/src/credential-did/credential-did.controller.ts b/src/credential-did/credential-did.controller.ts new file mode 100644 index 0000000..d5ec726 --- /dev/null +++ b/src/credential-did/credential-did.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, HttpStatus, Logger, Post, Res, UseInterceptors } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { CredentialDIDService } from "./credential-did.service"; +import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; +import { CreateUpdateDIDDto } from "./dto"; + +@Controller("credentialDID") +@ApiTags("credentialDID") +export class CredentialDIDController { + private readonly logger = new Logger(CredentialDIDController.name); + constructor(private readonly credentialDIDService: CredentialDIDService) {} + + @Post() + async createOrUpdateDIDs(@Res() res, @Body() createUpdateDID: CreateUpdateDIDDto) { + // Log the initiaton for csv file upload + this.logger.log(`Creating or updating DIDs`); + try { + const result = await this.credentialDIDService.createOrUpdateDIDs(createUpdateDID) + this.logger.log(`Created or updated DIDs successfully.`); + return res.status(HttpStatus.OK).json({ + message: "Created or updated DIDs successfully.", + }); + } catch (error) { + this.logger.error(`Failed to create or update DIDs..`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create or update DIDs.`, + }); + } + } +} diff --git a/src/credential-did/credential-did.module.ts b/src/credential-did/credential-did.module.ts new file mode 100644 index 0000000..01a7dd1 --- /dev/null +++ b/src/credential-did/credential-did.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { CredentialDIDService } from "./credential-did.service"; +import { CredentialDIDController } from "./credential-did.controller"; +import { PrismaModule } from "src/prisma/prisma.module"; +import { SunbirdRcService } from "src/external-services/sunbird-rc/sunbird-rc.service"; + +@Module({ + imports: [PrismaModule], + controllers: [CredentialDIDController], + providers: [CredentialDIDService, SunbirdRcService], + exports: [CredentialDIDService], +}) +export class CredentialDIDModule {} diff --git a/src/credential-did/credential-did.service.ts b/src/credential-did/credential-did.service.ts new file mode 100644 index 0000000..3853b6e --- /dev/null +++ b/src/credential-did/credential-did.service.ts @@ -0,0 +1,49 @@ +import { + ForbiddenException, + Inject, + Injectable, + forwardRef +} from "@nestjs/common"; +import { SunbirdRcService } from "src/external-services/sunbird-rc/sunbird-rc.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { CreateUpdateDIDDto } from "./dto"; + +@Injectable() +export class CredentialDIDService { + constructor( + private prisma: PrismaService, + @Inject(forwardRef(()=>SunbirdRcService)) + private sunbirdRc: SunbirdRcService + ) {} + + async createOrUpdateDIDs(createOrUpdateDID: CreateUpdateDIDDto) { + try { + await this.sunbirdRc.resolveDid(createOrUpdateDID.authorDid); + await this.sunbirdRc.fetchCredSchemaByIdAndVersion(createOrUpdateDID.schemaDid, createOrUpdateDID.schemaVersion); + const did = await this.prisma.credentialDid.findFirst(); + + if (!did) { + return await this.prisma.credentialDid.create({ + data: createOrUpdateDID, + }); + } else { + return await this.prisma.credentialDid.update({ + where: { id: did.id }, + data: createOrUpdateDID, + }); + } + } catch (error) { + throw error; + } + } + + async findDIDs() { + const did = await this.prisma.credentialDid.findFirst(); + if (!did) { + throw new ForbiddenException( + `Please configure the schema and author DIDs (used to issue credentials).` + ); + } + return did; + } +} diff --git a/src/credential-did/dto/create-update-did.dto.ts b/src/credential-did/dto/create-update-did.dto.ts new file mode 100644 index 0000000..d0b1c70 --- /dev/null +++ b/src/credential-did/dto/create-update-did.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateUpdateDIDDto { + @ApiProperty({ required: false, description: 'Author DID' }) + @IsString() + @IsNotEmpty() + authorDid: string; + + @ApiProperty({ required: false, description: 'Schema DID' }) + @IsString() + @IsNotEmpty() + schemaDid: string; + + @ApiProperty({ required: false, description: 'Schema DID' }) + @IsString() + @IsNotEmpty() + schemaVersion: string; +} \ No newline at end of file diff --git a/src/credential-did/dto/index.ts b/src/credential-did/dto/index.ts new file mode 100644 index 0000000..43dc9ec --- /dev/null +++ b/src/credential-did/dto/index.ts @@ -0,0 +1 @@ +export * from "./create-update-did.dto" \ No newline at end of file diff --git a/src/external-services/external-services.module.ts b/src/external-services/external-services.module.ts new file mode 100644 index 0000000..b38d841 --- /dev/null +++ b/src/external-services/external-services.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { PassbookModule } from "./passbook/passbook.module"; +import { SunbirdRcModule } from "./sunbird-rc/sunbird-rc.module"; +import { TarentoModule } from "./tarento/tarento.module"; +import { RouterModule } from "@nestjs/core"; + +@Module({ + imports: [ + PassbookModule, + SunbirdRcModule, + TarentoModule, + RouterModule.register([ + { path: "ext-service/", module: TarentoModule }, + ]), + ], + exports: [PassbookModule, SunbirdRcModule, TarentoModule], +}) +export class ExternalServicesModule {} diff --git a/src/external-services/passbook/dto/index.ts b/src/external-services/passbook/dto/index.ts new file mode 100644 index 0000000..4438cc8 --- /dev/null +++ b/src/external-services/passbook/dto/index.ts @@ -0,0 +1 @@ +export * from './passbook.dto' \ No newline at end of file diff --git a/src/external-services/passbook/dto/passbook.dto.ts b/src/external-services/passbook/dto/passbook.dto.ts new file mode 100644 index 0000000..989f36d --- /dev/null +++ b/src/external-services/passbook/dto/passbook.dto.ts @@ -0,0 +1,9 @@ +import { + IsString, +} from "class-validator"; +import { SurveyScoreCredentailDto } from "src/external-services/sunbird-rc/dto"; + +export class AddFeedbackDto extends SurveyScoreCredentailDto { + @IsString() + certificateId: string; +} \ No newline at end of file diff --git a/src/external-services/passbook/passbook.module.ts b/src/external-services/passbook/passbook.module.ts new file mode 100644 index 0000000..dc814ac --- /dev/null +++ b/src/external-services/passbook/passbook.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PassbookService } from './passbook.service'; + +@Module({ + providers: [PassbookService], + exports: [PassbookService] +}) +export class PassbookModule {} diff --git a/src/external-services/passbook/passbook.service.ts b/src/external-services/passbook/passbook.service.ts new file mode 100644 index 0000000..e06a4f3 --- /dev/null +++ b/src/external-services/passbook/passbook.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from "@nestjs/common"; +import axios from "axios"; +import { AddFeedbackDto } from "./dto"; + +@Injectable() +export class PassbookService { + private apiUrl = process.env.PASSBOOK_SERVICE_URL; + + async getUser(userId: string): Promise { + try { + const response = await axios.get(`${this.apiUrl}/user`, { + params: { + userId: userId, + }, + }); + return response.data; + } catch (error) { + // Handle errors + throw new Error("Failed to fetch data from the external API"); + } + } + + async createUser(userId: string): Promise { + try { + const response = await axios.post(`${this.apiUrl}/user`, { + data: { + userId: userId, + }, + }); + return response.data; + } catch (error) { + // Handle errors + throw new Error("Failed to fetch data from the external API"); + } + } + + async addFeedback(feedback: AddFeedbackDto): Promise { + try { + const response = await axios.post(`${this.apiUrl}/user/feedback`, { + data: { + ...feedback + }, + }); + return response.data; + } catch (error) { + // Handle errors + throw new Error(`Failed to add feedback for user with id #${feedback.userId}`); + } + } +} diff --git a/src/external-services/sunbird-rc/dto/credentail-schema.dto.ts b/src/external-services/sunbird-rc/dto/credentail-schema.dto.ts new file mode 100644 index 0000000..46d794d --- /dev/null +++ b/src/external-services/sunbird-rc/dto/credentail-schema.dto.ts @@ -0,0 +1,106 @@ +import { + IsDateString, + IsInt, + Min, + Max, + IsString, + ValidateNested, + ArrayMinSize, + ArrayNotEmpty, + IsNumber, + IsOptional, + IsNotEmpty, + IsArray, + IsEnum, +} from "class-validator"; +import { Type } from "class-transformer"; + +class LevelCredentailDto { + @IsInt() + levelNumber: number; + + @IsString() + name: string; + + @IsOptional() + @IsNotEmpty() + @IsNumber() + @Min(0, { message: "Score must be at least 0" }) + @Max(100, { message: "Score must not exceed 100" }) + score: number | null; +} + +export class CompetencyCredentailDto { + @IsInt() + id: number; + + @IsString() + name: string; + + @ArrayMinSize(1, { message: "Levels must have at least one item" }) + @ArrayNotEmpty({ message: "Levels must not be empty" }) + @ValidateNested({ each: true }) + @Type(() => LevelCredentailDto) + levels: LevelCredentailDto[]; +} + +export class SurveyScoreCredentailDto { + @IsDateString() + dateOfSurveyScore: string; + + @IsOptional() + @IsNotEmpty() + @IsNumber() + @Min(0, { message: "Overall score must be at least 0" }) + @Max(100, { message: "Overall score must not exceed 100" }) + overallScore: number | null; + + @ArrayMinSize(1, { message: "Competencies must have at least one item" }) + @ArrayNotEmpty({ message: "Competencies must not be empty" }) + @ValidateNested({ each: true }) + @Type(() => CompetencyCredentailDto) + competencies: CompetencyCredentailDto[]; + + @IsString() + userId: string; +} + +enum CredentialStatus { + DRAFT = "DRAFT", + PUBLISHED = "PUBLISHED", + ARCHIVED = "ARCHIVED", +} + +export class createSchemaInputDTO { + @IsString() + @IsNotEmpty() + type: string; + + @IsString() + @IsNotEmpty() + version: string; + + @IsString() + @IsNotEmpty() + id: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + author: string; + + @IsDateString() + @IsNotEmpty() + authored: string; + + @IsArray() + @IsString({ each: true }) + tags: string[]; + + @IsEnum(CredentialStatus) + @IsNotEmpty() + status: CredentialStatus; +} diff --git a/src/external-services/sunbird-rc/dto/index.ts b/src/external-services/sunbird-rc/dto/index.ts new file mode 100644 index 0000000..23dc5c6 --- /dev/null +++ b/src/external-services/sunbird-rc/dto/index.ts @@ -0,0 +1 @@ +export * from './credentail-schema.dto'; \ No newline at end of file diff --git a/src/external-services/sunbird-rc/schema/index.ts b/src/external-services/sunbird-rc/schema/index.ts new file mode 100644 index 0000000..2a156a2 --- /dev/null +++ b/src/external-services/sunbird-rc/schema/index.ts @@ -0,0 +1 @@ +export * from "./wpcas-credential.schema"; \ No newline at end of file diff --git a/src/external-services/sunbird-rc/schema/wpcas-credential.schema.ts b/src/external-services/sunbird-rc/schema/wpcas-credential.schema.ts new file mode 100644 index 0000000..dc7dd08 --- /dev/null +++ b/src/external-services/sunbird-rc/schema/wpcas-credential.schema.ts @@ -0,0 +1,60 @@ +import { SurveyScoreCredentailDto } from "../dto"; + +export const schema = { + $schema: "https://json-schema.org/draft/2019-09/schema", + description: + "The individual has achieved a score in the most recent peer survey.", + type: "object", + properties: { + dateOfSurveyScore: { + type: "string", + }, + overallScore: { + type: "number", + minimum: 0, + maximum: 100, + }, + competencies: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + }, + levels: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + }, + score: { + type: "number", + minimum: 0, + maximum: 100, + }, + }, + required: ["name", "score"], + }, + }, + }, + required: ["id", "name", "levels"], + }, + }, + userId: { + type: "string", + }, + }, + required: ["dateOfSurveyScore", "overallScore", "competencies", "userId"], + additionalProperties: true, +}; + + +export const createSchema = (id: string) => { + return { + $id: id, + ...schema, + } +}; \ No newline at end of file diff --git a/src/external-services/sunbird-rc/sunbird-rc.module.ts b/src/external-services/sunbird-rc/sunbird-rc.module.ts new file mode 100644 index 0000000..22c8b9d --- /dev/null +++ b/src/external-services/sunbird-rc/sunbird-rc.module.ts @@ -0,0 +1,10 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { SunbirdRcService } from './sunbird-rc.service'; +import { CredentialDIDModule } from 'src/credential-did/credential-did.module'; +import { CredentialDIDService } from 'src/credential-did/credential-did.service'; + +@Module({ + providers: [CredentialDIDService, SunbirdRcService], + exports: [SunbirdRcService] +}) +export class SunbirdRcModule {} diff --git a/src/external-services/sunbird-rc/sunbird-rc.service.ts b/src/external-services/sunbird-rc/sunbird-rc.service.ts new file mode 100644 index 0000000..13149a9 --- /dev/null +++ b/src/external-services/sunbird-rc/sunbird-rc.service.ts @@ -0,0 +1,108 @@ +import { Inject, Injectable, NotAcceptableException, forwardRef } from "@nestjs/common"; +import axios from "axios"; +import { SurveyScoreCredentailDto } from "./dto"; +import { CredentialDIDService } from "src/credential-did/credential-did.service"; + +@Injectable() +export class SunbirdRcService { + constructor( + @Inject(forwardRef(()=>CredentialDIDService)) + private credentialDID: CredentialDIDService + ){} + private sunbirdRcUrl = process.env.SUNBIRD_SERVICE_URL; + + async fetchCredSchemaByIdAndVersion(schemaDid: string, version: string): Promise { + try { + const response = await axios.get( + `${this.sunbirdRcUrl}/credential-schema/${schemaDid}/${version}` + ); + if(response.status != 200){ + throw new NotAcceptableException(`Authentication failed for the Schema with did #${schemaDid} and version #${version}`); + } + return response.data; + } catch (error) { + // Handle errors + throw new Error(`Failed to fetch data from the external API(fetchCredSchemaByIdAndVersion). Error: ${error.message}`); + } + } + + async issueCredential(credentialData: SurveyScoreCredentailDto): Promise { + try { + + const dids = await this.credentialDID.findDIDs(); + const creationDate = new Date(); + const expirationDate = new Date(); + expirationDate.setFullYear(creationDate.getFullYear() + 1); + + const response = await axios.post( + `${this.sunbirdRcUrl}/credentials/issue`, + { + credential: { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + type: ["VerifiableCredential", "WPCASSurveyScoreCredential"], + issuer: dids.authorDid, + issuanceDate: creationDate, + expirationDate: expirationDate, + credentialSubject: { + id: dids.authorDid, + ...credentialData + }, + options: { + created: creationDate.toISOString(), + credentialStatus: { + type: "RevocationList2020Status", + }, + }, + }, + credentialSchemaId: dids.schemaDid, + credentialSchemaVersion: dids.schemaVersion, + tags: ["tag1", "tag2", "tag3"], + } + ); + return response.data; + } catch (error) { + // Handle errors + throw new Error(`Failed to issue credentials for user with id #${credentialData.userId}. Error: ${error.message}`); + } + } + + // async verifyCredentail(did: string): Promise { + // try { + // const response = await axios.post( + // `${this.sunbirdRcUrl}/credentials/${did}/verify` + // ); + // return response.data; + // } catch (error) { + // // Handle errors + // throw new Error("Failed to fetch data from the external API(verifyCredentail)"); + // } + // } + + async resolveDid(did: string): Promise { + try { + const response = await axios.get( + `${this.sunbirdRcUrl}/did/resolve/${did}` + ); + if(response.status != 200){ + throw new NotAcceptableException(`Authentication failed for the did #${did}`); + } + return response.data; + } catch (error) { + // Handle errors + throw new Error(`Failed to fetch data from the external API(resolveDid). Error: ${error.message}`); + } + } + + // async getCredentailById(credentialDID: string): Promise { + // try { + // const response = await axios.get(`${this.sunbirdRcUrl}/credentials/${credentialDID}`); + // return response.data; + // } catch (error) { + // // Handle errors + // throw new Error("Failed to fetch data from the external API(getCredentailById)"); + // } + // } +} diff --git a/src/external-services/tarento/tarento.controller.ts b/src/external-services/tarento/tarento.controller.ts new file mode 100644 index 0000000..77e5f4a --- /dev/null +++ b/src/external-services/tarento/tarento.controller.ts @@ -0,0 +1,36 @@ +import { Controller, HttpStatus, Post, Res } from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { TarentoService } from "./tarento.service"; +import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; + +@Controller("tarento") +@ApiTags("ext-service/tarento") +export class TarentoController { + constructor(private readonly tarentoService: TarentoService) {} + + @Post("syncFracData") + @ApiOperation({ summary: "Sync FRAC data." }) + @ApiResponse({ + status: HttpStatus.OK, + isArray: true, + }) + async findAll(@Res() res) { + try { + const response = await this.tarentoService.formatAndSyncFracData(); + console.log(response); + return res.status(HttpStatus.OK).json({ + message: response, + }); + } catch (error) { + console.error(`Failed to create new survey score`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create survey score`, + }); + } + } +} diff --git a/src/external-services/tarento/tarento.module.ts b/src/external-services/tarento/tarento.module.ts new file mode 100644 index 0000000..4acde87 --- /dev/null +++ b/src/external-services/tarento/tarento.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { TarentoService } from "./tarento.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { TarentoController } from "./tarento.controller"; + +@Module({ + controllers:[TarentoController], + providers: [TarentoService, PrismaService], + exports: [TarentoService] +}) +export class TarentoModule {} \ No newline at end of file diff --git a/src/external-services/tarento/tarento.service.ts b/src/external-services/tarento/tarento.service.ts new file mode 100644 index 0000000..c0c0190 --- /dev/null +++ b/src/external-services/tarento/tarento.service.ts @@ -0,0 +1,425 @@ +import { Injectable } from "@nestjs/common"; +import { Competency, CompetencyLevel, Designation, Role } from "@prisma/client"; +import axios from "axios"; +import _ from "lodash"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class TarentoService { + constructor(private prisma: PrismaService) {} + private fracApiUrl = process.env.FRAC_SERVICE_URL; + + async getAllUsers(): Promise { + try { + const baseUrl = process.env.USER_SERVICE_URL || ""; + if (baseUrl == "") { + throw new Error("User service URL not available"); + } + const headers = { + Authorization: "bearer " + process.env.USER_SERVICE_TOKEN, + "Content-Type": "application/json", + }; + + const data = { + request: { + filters: {}, + }, + }; + + let response = await axios.post(baseUrl, data, { headers }); + + if (response.data.result.response.content.lenght < 1) { + throw new Error("Zero users fetched."); + } + + return response.data.result.response.content; + } catch (error) { + // Handle errors + console.log(error); + throw new Error("Failed to fetch user data from the Tarento's API"); + } + } + + async getUser(userId: string): Promise { + try { + const baseUrl = process.env.USER_SERVICE_URL || ""; + if (baseUrl == "") { + throw new Error("User service URL not available"); + } + const headers = { + Authorization: "bearer " + process.env.USER_SERVICE_TOKEN, + "Content-Type": "application/json", + }; + + const data = { + request: { + filters: { + id: userId, + }, + }, + }; + + let response = await axios.post(baseUrl, data, { headers }); + + return response; + } catch (error) { + // Handle errors + console.log(error); + throw new Error("Failed to fetch user data from the Tarento's API"); + } + } + + async getFracData(): Promise { + try { + const response = await axios.get(`${this.fracApiUrl}`, { + headers: { + Accept: "application/json", + "Accept-Language": "en-GB,en;q=0.9", + Cookie: + "connect.sid=s%3A3R7JwXRshJ5kaAM2AoHznye9VSbWIoks.CTD%2BcCE9y%2FDYcQ6FhBS5Fi2cFbl5h9SAIA51Me%2FzU3g", + }, + }); + return response.data.result.framework.categories; + } catch (error) { + // Handle errors + throw new Error("Failed to fetch FRAC data from the Tarento's API"); + } + } + + async formatAndSyncFracData(): Promise { + try { + const fracData = await this.getFracData(); + + await this.prisma.$transaction(async (prismaClient) => { + //sync designations(positions) + const designations = _.find(fracData, { + name: "Positions", + status: "Live", + }); + const syncedDesignations: Designation[] = []; + + for (const position of designations.terms) { + if (position.status === "Live") { + const designationDTO = { + name: position.name, + description: position.description, + }; + + try { + const result = await prismaClient.designation.upsert({ + where: { + name: designationDTO.name, + }, + create: designationDTO, + update: designationDTO, + }); + syncedDesignations.push(result); + } catch (error) { + console.error("Error upserting designation:", error); + throw new Error(error); + } + } + } + + // console.log("syncedDesignations", syncedDesignations); + + //sync roles + const roles = _.find(fracData, { name: "Roles", status: "Live" }); + const syncedRoles: Role[] = []; + + for (const role of roles.terms) { + if (role.status === "Live") { + const roleDTO = { + name: role.name, + description: role.description, + }; + + try { + const syncedRole = await prismaClient.role.upsert({ + where: { + name: roleDTO.name, + }, + create: roleDTO, + update: roleDTO, + }); + syncedRoles.push(syncedRole); + } catch (error) { + console.error("Error upserting role:", error); + throw new Error(error); + } + } + } + + // console.log("syncedRoles", syncedRoles); + + //map designatioToRoles + for (const position of designations.terms) { + if (position.associations.length > 0) { + const positionToBeMapped = _.find(syncedDesignations, { + name: position.name, + description: position.description, + }); + // console.log("positionToBeMapped: ", positionToBeMapped); + + for (const roleToBeMapped of position.associations) { + // console.log("roleToBeMapped: ", roleToBeMapped); + if (roleToBeMapped.status == "Live") { + const roleAdded = await prismaClient.role.findUnique({ + where: { + name: roleToBeMapped.name, + description: roleToBeMapped.description, + }, + }); + // console.log("roleBeingMapped: ", roleAdded); + + if (roleAdded != null && positionToBeMapped != null) { + let designationId = positionToBeMapped.id; + let roleId = roleAdded.id; + + console.log( + "designationToBeMapped: ", + positionToBeMapped.name + ); + console.log("roleBeingMapped: ", roleAdded.name); + + try { + await prismaClient.designationToRole.upsert({ + where: { + designationId_roleId: { + designationId, + roleId, + }, + }, + update: { + designationId, + roleId, + }, + create: { + designationId, + roleId, + }, + }); + } catch (error) { + console.error( + "Error syncing competenciesToCompetencyLevels:", + error + ); + throw new Error(error); + } + } + } + } + } + } + + console.log("Successfully mapped designatioToRoles"); + + //sync competencyLevels + const competencyLevels = _.find(fracData, { + name: "Competency Levels", + status: "Live", + }); + const syncedCompetencyLevels: CompetencyLevel[] = []; + + for (const competencyLevel of competencyLevels.terms) { + if (competencyLevel.status === "Live") { + const competencyLevelDTO = { + name: competencyLevel.name, + description: competencyLevel.description, + levelNumber: + competencyLevel.moreProperties.levelNumber || + competencyLevel.index, + }; + + try { + const upsertResult = await prismaClient.competencyLevel.upsert({ + where: { + name: competencyLevelDTO.name, + }, + create: competencyLevelDTO, + update: competencyLevelDTO, + }); + + syncedCompetencyLevels.push(upsertResult); + } catch (error) { + console.error("Error upserting role:", error); + throw new Error(error); + } + } + } + + // console.log("syncedCompetencyLevels", syncedCompetencyLevels); + + //sync competencies + const competencies = _.find(fracData, { + name: "Competencies", + status: "Live", + }); + const syncedCompetencies: Competency[] = []; + for (const competency of competencies.terms) { + if (competency.status === "Live") { + const competencyDTO = { + name: competency.name, + description: competency.description, + }; + + try { + const result = await prismaClient.competency.upsert({ + where: { + name: competencyDTO.name, + }, + create: competencyDTO, + update: competencyDTO, + }); + + syncedCompetencies.push(result); + } catch (error) { + console.error("Error upserting competency:", error); + throw new Error(error); + } + } + } + + // console.log("syncedCompetency", syncedCompetency); + + //map competenciesToCompetencyLevels + await Promise.all( + competencies.terms.map(async (competency) => { + if (competency.associations && competency.associations.length > 0) { + const competencyToBeMapped = _.find(syncedCompetencies, { + name: competency.name, + description: competency.description, + }); + competency.associations.forEach( + async (competencyLevelToBeMapped) => { + if (competencyLevelToBeMapped.status == "Live") { + const competencyLevelAdded = _.find( + syncedCompetencyLevels, + { + name: competencyLevelToBeMapped.name, + } + ); + if (competencyLevelAdded && competencyToBeMapped) { + const competencyId = competencyToBeMapped?.id; + const competencyLevelId = competencyLevelAdded?.id; + + try { + await prismaClient.competencyToCompetencyLevel.upsert({ + where: { + competencyId_competencyLevelId: { + competencyId, + competencyLevelId, + }, + }, + update: { + competencyId, + competencyLevelId, + }, + create: { + competencyId, + competencyLevelId, + }, + }); + } catch (error) { + console.error( + "Error syncing competenciesToCompetencyLevels:", + error + ); + throw new Error(error); + } + } + } + } + ); + } + }) + ); + + console.log("Successfully mapped competenciesToCompetencyLevels"); + + //map rolesToCompetencies + const activities = _.find(fracData, { + name: "Activities", + status: "Live", + }); + for (const role of roles.terms) { + if (role.associations && role.associations.length > 0) { + let activitiesToBeAdded: string[] = []; + const roleToBeMapped = _.find(syncedRoles, { + name: role.name, + description: role.description, + }); + if (roleToBeMapped) { + for (const activity of role.associations) { + if (activity.status === "Live") { + const activityWithAssociations = _.find(activities.terms, { + name: activity.name, + description: activity.description, + }); + activitiesToBeAdded.push(activity.name); + if ( + activityWithAssociations && + activityWithAssociations.associations && + activityWithAssociations.associations.length > 0 + ) { + for (const competency of activityWithAssociations.associations) { + if (competency.status === "Live") { + const competencyAdded = _.find(syncedCompetencies, { + name: competency.name, + }); + if (competencyAdded) { + const roleId = roleToBeMapped.id; + const competencyId = competencyAdded.id; + + try { + await prismaClient.roleToCompetency.upsert({ + where: { + roleId_competencyId: { + roleId, + competencyId, + }, + }, + update: { + roleId, + competencyId, + }, + create: { + roleId, + competencyId, + }, + }); + } catch (error) { + console.error( + "Error syncing rolesToCompetencies:", + error + ); + throw new Error(error); + } + } + } + } + } + } + } + console.log("activitiesToBeAdded: ", activitiesToBeAdded); + await prismaClient.role.update({ + where: { + id: roleToBeMapped.id, + }, + data: { + activities: activitiesToBeAdded, + }, + }); + } + } + } + console.log("Successfully mapped rolesToCompetencies"); + console.log("Successfully mapped activities to roles"); + }); + + return "FRAC data synced successfully"; + } catch (error) { + throw new Error(error); + } + } +} diff --git a/src/file-upload/dto/create-file-upload.dto.ts b/src/file-upload/dto/create-file-upload.dto.ts new file mode 100644 index 0000000..54acc2e --- /dev/null +++ b/src/file-upload/dto/create-file-upload.dto.ts @@ -0,0 +1 @@ +export class CreateFileUploadDto {} diff --git a/src/file-upload/dto/update-file-upload.dto.ts b/src/file-upload/dto/update-file-upload.dto.ts new file mode 100644 index 0000000..095d832 --- /dev/null +++ b/src/file-upload/dto/update-file-upload.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateFileUploadDto } from './create-file-upload.dto'; + +export class UpdateFileUploadDto extends PartialType(CreateFileUploadDto) {} diff --git a/src/file-upload/file-upload.controller.ts b/src/file-upload/file-upload.controller.ts new file mode 100644 index 0000000..7452562 --- /dev/null +++ b/src/file-upload/file-upload.controller.ts @@ -0,0 +1,10 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { FileUploadService } from './file-upload.service'; +import { CreateFileUploadDto } from './dto/create-file-upload.dto'; +import { UpdateFileUploadDto } from './dto/update-file-upload.dto'; + +@Controller('file-upload') +export class FileUploadController { + constructor(private readonly fileUploadService: FileUploadService) {} + +} diff --git a/src/file-upload/file-upload.module.ts b/src/file-upload/file-upload.module.ts new file mode 100644 index 0000000..e31bd76 --- /dev/null +++ b/src/file-upload/file-upload.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { FileUploadService } from "./file-upload.service"; +import { FileUploadController } from "./file-upload.controller"; + +@Module({ + controllers: [FileUploadController], + providers: [FileUploadService], + exports: [FileUploadService], +}) +export class FileUploadModule {} diff --git a/src/file-upload/file-upload.service.ts b/src/file-upload/file-upload.service.ts new file mode 100644 index 0000000..502e38c --- /dev/null +++ b/src/file-upload/file-upload.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import * as fs from "fs"; +import csvParser = require("csv-parser"); + +@Injectable() +export class FileUploadService { + public async parseCSV(filePath: string): Promise { + const results: any[] = []; + + return new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(csvParser()) + .on("data", (row) => { + results.push(row); + }) + .on("end", () => { + resolve(results); + }) + .on("error", (error) => { + reject(error); + }); + }); + } + + public async deleteUploadedFile(filePath: string): Promise { + try { + fs.unlinkSync(filePath); + } catch (error) { + throw new Error("Error unlinking the file."); + } + } +} diff --git a/src/main.ts b/src/main.ts index 91e7dcd..850e1b6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,14 @@ async function bootstrap() { // Create a NestJS application instance const app = await NestFactory.create(AppModule, { cors: true }); + // Enable Cross-Origin Resource Sharing (CORS) + app.enableCors({ + origin: true, + methods: ["GET", "POST","PUT","PATCH", "DELETE"], + allowedHeaders: "*", + credentials:true + }); + // Enable using the container for class-validator useContainer(app.select(AppModule), { fallbackOnErrors: true }); @@ -39,7 +47,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe(validationOptions)); // Configure global serialization using class-transformer - app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + // app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // Configure Swagger documentation const swaggerConfig = new DocumentBuilder() @@ -72,7 +80,15 @@ async function bootstrap() { }); // Start the NestJS application, listening on the specified port or defaulting to 4010 - await app.listen(configService.get("APP_PORT") || 4010); + const PORT = configService.get("APP_PORT") || 4010; + await app.listen(PORT, () => { + console.log( + ` + 🔌 REST API ready at http://localhost:${PORT}/ + 💻 Swagger UI ready at http://localhost:${PORT}/${SWAGGER_CONSTANTS.PATH} + ` + ); + }); } // Call the bootstrap function to start the application diff --git a/src/mockModules/mock-competency-level/dto/create-competency-level.dto.ts b/src/mockModules/mock-competency-level/dto/create-competency-level.dto.ts new file mode 100644 index 0000000..3dbc6bb --- /dev/null +++ b/src/mockModules/mock-competency-level/dto/create-competency-level.dto.ts @@ -0,0 +1,17 @@ + +import { IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; + +export class CreateCompetencyLevelDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + description?: string; + + @IsNumber() + @IsNotEmpty() + levelNumber: number; +} \ No newline at end of file diff --git a/src/mockModules/mock-competency-level/dto/index.ts b/src/mockModules/mock-competency-level/dto/index.ts new file mode 100644 index 0000000..a9f39a6 --- /dev/null +++ b/src/mockModules/mock-competency-level/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-competency-level.dto'; +export * from './response-competency-level.dto'; +export * from './update-competency-level.dto'; \ No newline at end of file diff --git a/src/mockModules/mock-competency-level/dto/response-competency-level.dto.ts b/src/mockModules/mock-competency-level/dto/response-competency-level.dto.ts new file mode 100644 index 0000000..27aab6f --- /dev/null +++ b/src/mockModules/mock-competency-level/dto/response-competency-level.dto.ts @@ -0,0 +1,8 @@ +import { ResponseCompetencyDto } from "src/mockModules/mock-competency/dto"; + +export class ResponseCompetencyLevelDto { + readonly id: number; + readonly name: string; + readonly description?: string; + readonly competencies?: ResponseCompetencyDto[] + } \ No newline at end of file diff --git a/src/mockModules/mock-competency-level/dto/update-competency-level.dto.ts b/src/mockModules/mock-competency-level/dto/update-competency-level.dto.ts new file mode 100644 index 0000000..20c315f --- /dev/null +++ b/src/mockModules/mock-competency-level/dto/update-competency-level.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateCompetencyLevelDto } from './create-competency-level.dto'; + +export class UpdateCompetencyLevelDto extends PartialType(CreateCompetencyLevelDto) {} \ No newline at end of file diff --git a/src/mockModules/mock-competency-level/mock-competency-level.controller.ts b/src/mockModules/mock-competency-level/mock-competency-level.controller.ts new file mode 100644 index 0000000..0be1f0f --- /dev/null +++ b/src/mockModules/mock-competency-level/mock-competency-level.controller.ts @@ -0,0 +1,136 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + ParseIntPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { MockCompetencyLevelService } from "./mock-competency-level.service"; +import { + CreateCompetencyLevelDto, + ResponseCompetencyLevelDto, + UpdateCompetencyLevelDto, +} from "./dto"; + +@Controller("competency-level") +@ApiTags("mockFracService/competency-level") +export class MockCompetencyLevelController { + constructor(private competencyLevelService: MockCompetencyLevelService) {} + @Post() + @ApiOperation({ summary: "create new mock competency-level" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseCompetencyLevelDto }) + async createCompetencyLevel( + @Res() res, + @Body() createCompetencyLevelDto: CreateCompetencyLevelDto + ) { + try { + const competency = + await this.competencyLevelService.createCompetencyLevel( + createCompetencyLevelDto + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "CompetencyLevel successfully created.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get() + @ApiOperation({ summary: "fetch all mock competency-levels" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseCompetencyLevelDto, + isArray: true, + }) + async getAllCompetencyLevels(@Res() res) { + try { + const competency = + await this.competencyLevelService.findAllCompetencyLevels(); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "CompetencyLevels successfully fetched.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get(":id") + @ApiOperation({ summary: "get mock competency-level by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyLevelDto }) + async findOneCompetencyLevel( + @Res() res, + @Param("id", ParseIntPipe) id: number + ) { + try { + const competency = + await this.competencyLevelService.findCompetencyLevelById(id); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "CompetencyLevel successfully fetched.", + }); + } catch (error) { + return res + .status(error.response.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.response.message ?? "Internal server error." }); + } + } + + @Patch(":id") + @ApiOperation({ summary: "update mock competency-level by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyLevelDto }) + async updateCompetencyLevel( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateMockCompetencyLevelDto: UpdateCompetencyLevelDto + ) { + try { + const competency = + await this.competencyLevelService.updateCompetencyLevelById( + id, + updateMockCompetencyLevelDto + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "CompetencyLevels successfully updated.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "delete mock competency-level by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyLevelDto }) + async removeCompetencyLevel( + @Res() res, + @Param("id", ParseIntPipe) id: number + ) { + try { + const competency = + await this.competencyLevelService.removeCompetencyLevel(id); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "CompetencyLevels successfully deleted.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } +} diff --git a/src/mockModules/mock-competency-level/mock-competency-level.module.ts b/src/mockModules/mock-competency-level/mock-competency-level.module.ts new file mode 100644 index 0000000..1a1d266 --- /dev/null +++ b/src/mockModules/mock-competency-level/mock-competency-level.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MockCompetencyLevelController } from './mock-competency-level.controller'; +import { MockCompetencyLevelService } from './mock-competency-level.service'; +import { PrismaModule } from '../../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [MockCompetencyLevelController], + providers: [MockCompetencyLevelService], +}) +export class MockCompetencyLevelModule {} diff --git a/src/mockModules/mock-competency-level/mock-competency-level.service.ts b/src/mockModules/mock-competency-level/mock-competency-level.service.ts new file mode 100644 index 0000000..0318705 --- /dev/null +++ b/src/mockModules/mock-competency-level/mock-competency-level.service.ts @@ -0,0 +1,60 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; +import { CreateCompetencyLevelDto, UpdateCompetencyLevelDto } from "./dto"; + +@Injectable() +export class MockCompetencyLevelService { + constructor(private prisma: PrismaService) {} + public async createCompetencyLevel( + createCompetencyLevelDto: CreateCompetencyLevelDto + ) { + return this.prisma.competencyLevel.create({ + data: createCompetencyLevelDto, + }); + } + + public async findAllCompetencyLevels() { + return this.prisma.competencyLevel.findMany(); + } + + public async findCompetencyLevelById(id: number) { + const competencyLevel = await this.prisma.competencyLevel.findUnique({ + where: { + id, + }, + select: { + id: true, + name: true, + description: true, + competencies: { + select: { + competency: true, + }, + }, + }, + }); + if (!competencyLevel) + throw new NotFoundException(`CompetencyLevel with id #${id} not found`); + return competencyLevel; + } + + public async updateCompetencyLevelById( + id: number, + updateCompetencyLevelDto: UpdateCompetencyLevelDto + ) { + return this.prisma.competencyLevel.update({ + where: { + id, + }, + data: updateCompetencyLevelDto, + }); + } + + public async removeCompetencyLevel(id: number) { + return this.prisma.competencyLevel.delete({ + where: { + id, + }, + }); + } +} diff --git a/src/mockModules/mock-competency/dto/create-competency.dto.ts b/src/mockModules/mock-competency/dto/create-competency.dto.ts new file mode 100644 index 0000000..898d1b0 --- /dev/null +++ b/src/mockModules/mock-competency/dto/create-competency.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; + +export class CreateCompetencyDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + description?: string; +} + +export class AddCompetencyLevelDto { + @IsNotEmpty() + @IsNumber() + competencyLevelId: number; +} diff --git a/src/mockModules/mock-competency/dto/index.ts b/src/mockModules/mock-competency/dto/index.ts new file mode 100644 index 0000000..41b680c --- /dev/null +++ b/src/mockModules/mock-competency/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-competency.dto'; +export * from './response-competency.dto'; +export * from './update-competency.dto'; \ No newline at end of file diff --git a/src/mockModules/mock-competency/dto/response-competency.dto.ts b/src/mockModules/mock-competency/dto/response-competency.dto.ts new file mode 100644 index 0000000..aa69cb3 --- /dev/null +++ b/src/mockModules/mock-competency/dto/response-competency.dto.ts @@ -0,0 +1,15 @@ +import { ResponseCompetencyLevelDto } from "src/mockModules/mock-competency-level/dto"; +import { ResponseMockRoleDto } from "src/mockModules/mock-role/dto"; + +export class ResponseCompetencyDto { + readonly id: number; + readonly name: string; + readonly description?: string; + readonly roles?: ResponseMockRoleDto[]; + readonly competencies?: ResponseCompetencyDto[]; +} + +export class ResponseAddCompetencyLevelToCompetency { + readonly competency: ResponseCompetencyDto; + readonly competencyLevel?: ResponseCompetencyLevelDto; +} diff --git a/src/mockModules/mock-competency/dto/update-competency.dto.ts b/src/mockModules/mock-competency/dto/update-competency.dto.ts new file mode 100644 index 0000000..06234aa --- /dev/null +++ b/src/mockModules/mock-competency/dto/update-competency.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateCompetencyDto } from './create-competency.dto'; + +export class UpdateCompetencyDto extends PartialType(CreateCompetencyDto) {} \ No newline at end of file diff --git a/src/mockModules/mock-competency/mock-competency.controller.ts b/src/mockModules/mock-competency/mock-competency.controller.ts new file mode 100644 index 0000000..52d5761 --- /dev/null +++ b/src/mockModules/mock-competency/mock-competency.controller.ts @@ -0,0 +1,212 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + ParseIntPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { + AddCompetencyLevelDto, + CreateCompetencyDto, + ResponseAddCompetencyLevelToCompetency, + ResponseCompetencyDto, + UpdateCompetencyDto, +} from "./dto"; +import { MockCompetencyService } from "./mock-competency.service"; +import { CreateCompetencyLevelDto } from "../mock-competency-level/dto"; + +@Controller("competency") +@ApiTags("mockFracService/competency") +export class MockCompetencyController { + constructor(private competencyService: MockCompetencyService) {} + + @Post() + @ApiOperation({ summary: "create new mock competency" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseCompetencyDto }) + async createCompetency( + @Res() res, + @Body() createCompetencyDto: CreateCompetencyDto + ) { + try { + const competency = await this.competencyService.createCompetency( + createCompetencyDto + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competency successfully created.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get() + @ApiOperation({ summary: "fetch all mock competencies" }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ResponseCompetencyDto, + isArray: true, + }) + async getAllCompetencies(@Res() res) { + try { + const competency = await this.competencyService.findAllCompetencies(); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competencies successfully fetched.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get("competencyWithLevels") + @ApiOperation({ summary: "fetch all mock competencies with levels" }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ResponseCompetencyDto, + isArray: true, + }) + async getAllCompetenciesWithLevels(@Res() res) { + try { + const competency = await this.competencyService.findCompetenciesWithLevelNames(); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competencies with levels successfully fetched.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get(":id") + @ApiOperation({ summary: "get mock competency by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyDto }) + async findOneCompetency(@Res() res, @Param("id", ParseIntPipe) id: number) { + try { + const competency = await this.competencyService.findCompetencyById(id); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competency successfully fetched.", + }); + } catch (error) { + return res + .status(error.response.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.response.message ?? "Internal server error." }); + } + } + + @Patch(":id") + @ApiOperation({ summary: "update mock competency by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyDto }) + async updateCompetency( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateCompetencyDto: UpdateCompetencyDto + ) { + try { + const competency = await this.competencyService.updatecompetencyById( + id, + updateCompetencyDto + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competency successfully updated.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "delete mock competency by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyDto }) + async removeCompetency(@Res() res, @Param("id", ParseIntPipe) id: number) { + try { + const competency = await this.competencyService.removeCompetency(id); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competency successfully deleted.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Post("addExistingCompetencyLevelToCompetency/:id") + @ApiOperation({ summary: "add competency level to competency" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseAddCompetencyLevelToCompetency, + }) + async addExistingCompetencyLevelToCompetency( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() competencyLevel: AddCompetencyLevelDto + ) { + try { + const competency = + await this.competencyService.addExistingCompetencyLevelToCompetency( + id, + competencyLevel.competencyLevelId + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: "Competency added competency level to competency.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ + message: + error?.meta?.cause || + `Failed to add competency level to competency`, + }); + } + } + + @Post("addNewCompetencyLevelToCompetency/:id") + @ApiOperation({ summary: "create and add competency level to competency" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseCompetencyDto }) + async addNewCompetencyLevelToCompetency( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() competencyLevel: CreateCompetencyLevelDto + ) { + try { + const competency = + await this.competencyService.addNewCompetencyLevelToCompetency( + id, + competencyLevel + ); + return res.status(HttpStatus.OK).json({ + data: competency, + message: + "Successfully created competency level and added to competency.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ + message: + error?.meta?.cause || + `Failed to create competency level and add to competency`, + }); + } + } +} diff --git a/src/mockModules/mock-competency/mock-competency.module.ts b/src/mockModules/mock-competency/mock-competency.module.ts new file mode 100644 index 0000000..8904c06 --- /dev/null +++ b/src/mockModules/mock-competency/mock-competency.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { MockCompetencyLevelService } from "../mock-competency-level/mock-competency-level.service"; +import { MockCompetencyController } from "./mock-competency.controller"; +import { MockCompetencyService } from "./mock-competency.service"; +import { PrismaModule } from "../../prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [MockCompetencyController], + providers: [MockCompetencyService, MockCompetencyLevelService], + exports: [MockCompetencyService], +}) +export class MockCompetencyModule {} diff --git a/src/mockModules/mock-competency/mock-competency.service.ts b/src/mockModules/mock-competency/mock-competency.service.ts new file mode 100644 index 0000000..25a52ab --- /dev/null +++ b/src/mockModules/mock-competency/mock-competency.service.ts @@ -0,0 +1,187 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; +import { CreateCompetencyLevelDto } from "../mock-competency-level/dto"; +import { MockCompetencyLevelService } from "../mock-competency-level/mock-competency-level.service"; +import { CreateCompetencyDto, UpdateCompetencyDto } from "./dto"; + +@Injectable() +export class MockCompetencyService { + constructor( + private prisma: PrismaService, + private competencyLevelService: MockCompetencyLevelService + ) {} + public async createCompetency(createMockRoleDto: CreateCompetencyDto) { + return this.prisma.competency.create({ + data: createMockRoleDto, + }); + } + + public async findAllCompetencies() { + return this.prisma.competency.findMany(); + } + + public async findCompetencyById(id: number) { + const competency = await this.prisma.competency.findUnique({ + where: { + id, + }, + select: { + id: true, + name: true, + description: true, + roles: { + select: { + role: true, + }, + }, + competencyLevels: { + select: { + competencyLevel: true, + }, + }, + }, + }); + if (!competency) + throw new NotFoundException(`Competency with id #${id} not found`); + return competency; + } + + public async updatecompetencyById( + id: number, + updateCompetencyDto: UpdateCompetencyDto + ) { + return this.prisma.competency.update({ + where: { + id, + }, + data: updateCompetencyDto, + }); + } + + public async removeCompetency(id: number) { + return this.prisma.competency.delete({ + where: { + id, + }, + }); + } + + public async addExistingCompetencyLevelToCompetency( + competencyId: number, + competencyLevelId: number + ) { + const competency = await this.findCompetencyById(competencyId); + const competencyLevel = + await this.competencyLevelService.findCompetencyLevelById( + competencyLevelId + ); + const connection = await this.prisma.competencyToCompetencyLevel.create({ + data: { + competencyId: competency.id, + competencyLevelId: competencyLevel.id, + }, + select: { + competency: true, + competencyLevel: true, + }, + }); + return connection; + } + + public async addNewCompetencyLevelToCompetency( + competencyId: number, + createCompetencyLevel: CreateCompetencyLevelDto + ) { + return this.prisma.competency.update({ + where: { + id: competencyId, + }, + select: { + id: true, + name: true, + description: true, + competencyLevels: { + orderBy: { + competencyLevel: { + createdAt: "desc", + }, + }, + select: { + competencyLevel: true, + }, + }, + }, + data: { + competencyLevels: { + create: { + competencyLevel: { + create: { + ...createCompetencyLevel, + }, + }, + }, + }, + }, + }); + } + + public async findCompetencyByName(competencyName: string) { + return this.prisma.competency.findUnique({ + where: { + name: competencyName, + }, + }); + } + + public async findAllCompetenciesWithLevelNames() { + return this.prisma.competency.findMany({ + include: { + competencyLevels: { + include: { + competencyLevel: true, + }, + }, + }, + }); + } + + public async findCompetenciesWithLevelNames() { + const competencies = await this.prisma.competency.findMany({ + select: { + id: true, + name: true, + competencyLevels: { + select: { + competencyLevel: { + select: { + id: true, + name: true, + levelNumber: true, + }, + }, + }, + }, + }, + }); + + return this.transformCompetencyObject(competencies); + } + + public transformCompetencyObject(competencies) { + const transformedCompetencies: any = []; + competencies.map(async (competency) => { + const transformedCompetency = { + id: competency.id, + name: competency.name, + levels: competency.competencyLevels.map((level) => ({ + id: level.competencyLevel.id, + name: level.competencyLevel.name, + levelNumber: level.competencyLevel.levelNumber, + })), + }; + transformedCompetencies.push(transformedCompetency); + }); + + return transformedCompetencies; + } +} diff --git a/src/mockModules/mock-designation/dto/create-designation.dto.ts b/src/mockModules/mock-designation/dto/create-designation.dto.ts new file mode 100644 index 0000000..a6dc501 --- /dev/null +++ b/src/mockModules/mock-designation/dto/create-designation.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class CreateDesignationDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsOptional() + @IsNotEmpty() + description?: string; +} diff --git a/src/mockModules/mock-designation/dto/index.ts b/src/mockModules/mock-designation/dto/index.ts new file mode 100644 index 0000000..f3d730c --- /dev/null +++ b/src/mockModules/mock-designation/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-designation.dto'; +export * from './update-designation.dto'; +export * from './response-designation.dto'; \ No newline at end of file diff --git a/src/mockModules/mock-designation/dto/response-designation.dto.ts b/src/mockModules/mock-designation/dto/response-designation.dto.ts new file mode 100644 index 0000000..44e81e9 --- /dev/null +++ b/src/mockModules/mock-designation/dto/response-designation.dto.ts @@ -0,0 +1,8 @@ +import { ResponseMockRoleDto } from "src/mockModules/mock-role/dto"; + +export class ResponseDesignationDto { + readonly id: number; + readonly name: string; + readonly description?: string | null; + readonly Role: ResponseMockRoleDto[] +} diff --git a/src/mockModules/mock-designation/dto/update-designation.dto.ts b/src/mockModules/mock-designation/dto/update-designation.dto.ts new file mode 100644 index 0000000..9d52fdc --- /dev/null +++ b/src/mockModules/mock-designation/dto/update-designation.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class UpdateDesignationDto { + @IsString() + @IsOptional() + @IsNotEmpty() + name?: string; + + @IsString() + @IsOptional() + @IsNotEmpty() + description?: string; +} diff --git a/src/mockModules/mock-designation/mock-designation.controller.ts b/src/mockModules/mock-designation/mock-designation.controller.ts new file mode 100644 index 0000000..1ce6b2f --- /dev/null +++ b/src/mockModules/mock-designation/mock-designation.controller.ts @@ -0,0 +1,136 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { MockDesignationService } from "./mock-designation.service"; +import { + CreateDesignationDto, + ResponseDesignationDto, + UpdateDesignationDto, +} from "./dto"; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; + +@Controller("designation") +@ApiTags("mockFracService/designation") +export class MockDesignationController { + constructor(private readonly designationService: MockDesignationService) {} + + @Post() + @ApiOperation({ summary: "Create mock designation." }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseDesignationDto }) + async create(@Body() createDto: CreateDesignationDto, @Res() res) { + try { + const designation = await this.designationService.createDesignation( + createDto + ); + return res.status(HttpStatus.CREATED).json({ + data: designation, + message: "Successfully created the Designation", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get() + @ApiOperation({ summary: "Fetch all mock designations." }) + @ApiResponse({ status: HttpStatus.FOUND, type: ResponseDesignationDto, isArray: true }) + async findAll(@Res() res): Promise { + try { + const designation = await this.designationService.findAllDesignations(); + return res.status(HttpStatus.FOUND).json({ + data: designation, + message: "Successfully Fetched all the designations.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get(":id") + @ApiOperation({ summary: "Fetch mock designation by id." }) + @ApiResponse({ status: HttpStatus.FOUND, type: ResponseDesignationDto}) + async findOne(@Param("id") id: number, @Res() res) { + try { + const designation = await this.designationService.findDesignationById(id); + return res.status(HttpStatus.FOUND).json({ + data: designation, + message: "Successfully Fetched the designation.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Patch(":id") + @ApiOperation({ summary: "Update mock designation by id." }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseDesignationDto}) + async update( + @Param("id") id: number, + @Body() updateDto: UpdateDesignationDto, + @Res() res + ) { + try { + const designation = await this.designationService.updateDesignation( + id, + updateDto + ); + return res.status(HttpStatus.OK).json({ + data: designation, + message: "Successfully updated the designation.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "Delete mock designation by id." }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseDesignationDto}) + async remove(@Param("id") id: number, @Res() res) { + try { + const designation = await this.designationService.removeDesignation(id); + return res.status(HttpStatus.OK).json({ + data: designation, + message: "Successfully deleted the designation.", + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Post("addRoleToDesignation/:id") + @ApiOperation({ summary: "Add a Role to mock designation." }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseDesignationDto}) + @ApiBody({ schema:{ example:{ roleId: 1 } } }) + async addRoleToDesignation(@Param("id") id: number, @Res() res, @Body() {roleId}: any) { + try { + const designation = await this.designationService.addRoleToDesignation(id, roleId); + return res.status(HttpStatus.OK).json({ + data: designation, + message: `Successfully added role with id #${roleId} to the designation with id #${id}.`, + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } +} diff --git a/src/mockModules/mock-designation/mock-designation.module.ts b/src/mockModules/mock-designation/mock-designation.module.ts new file mode 100644 index 0000000..255bfda --- /dev/null +++ b/src/mockModules/mock-designation/mock-designation.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { MockDesignationService } from './mock-designation.service'; +import { MockDesignationController } from './mock-designation.controller'; + +@Module({ + providers: [MockDesignationService], + controllers: [MockDesignationController] +}) +export class MockDesignationModule {} diff --git a/src/mockModules/mock-designation/mock-designation.service.ts b/src/mockModules/mock-designation/mock-designation.service.ts new file mode 100644 index 0000000..ee13b34 --- /dev/null +++ b/src/mockModules/mock-designation/mock-designation.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; +import { CreateDesignationDto, UpdateDesignationDto } from "./dto"; + +@Injectable() +export class MockDesignationService { + constructor(private prisma: PrismaService) {} + + async createDesignation(createDto: CreateDesignationDto) { + return await this.prisma.designation.create({ + data: createDto, + }); + } + + async findAllDesignations() { + return await this.prisma.designation.findMany(); + } + + async findDesignationById(id: number) { + return await this.prisma.designation.findUnique({ + where: { id }, + }); + } + + async findUsersWithDesignationId(id: number) { + const designation = await this.findDesignationById(id); + return await this.prisma.userMetadata.findMany({ + where: { + designation: designation?.name, + }, + }); + } + + async updateDesignation(id: number, updateDto: UpdateDesignationDto) { + return await this.prisma.designation.update({ + where: { id }, + data: updateDto, + }); + } + + async removeDesignation(id: number) { + return await this.prisma.designation.delete({ + where: { id }, + }); + } + + async addRoleToDesignation(id: number, roleId: number) { + try { + await this.prisma.designationToRole.upsert({ + where: { + designationId_roleId: { + designationId: id, + roleId: roleId, + }, + }, + update: { + designationId: id, + roleId: roleId, + }, + create: { + designationId: id, + roleId: roleId, + }, + }); + } catch (error) { + throw new Error("Error adding role to designation."); + } + + const designation = await this.prisma.designation.findUnique({ + where: { + id, + }, + select: { + id: true, + name: true, + description: true, + roles: { + select: { + role: { + select: { + id: true, + name: true, + description: true, + }, + }, + }, + }, + }, + }); + + return { + id: designation?.id, + name: designation?.name, + description: designation?.description, + Role: designation?.roles.map((item) => item.role), + }; + } + + async findAllRolesForDesignation(designation: string) { + const result = await this.prisma.designation.findUnique({ + where: { + name: designation, + }, + select: { + roles: { + select: { + role: true, + }, + }, + }, + }); + if (!result) return { Roles: [] }; + const roles = result.roles.map((item) => item.role); + return { Roles: roles }; + } +} diff --git a/src/mockModules/mock-role/dto/create-mock-role.dto.ts b/src/mockModules/mock-role/dto/create-mock-role.dto.ts new file mode 100644 index 0000000..ebe10bd --- /dev/null +++ b/src/mockModules/mock-role/dto/create-mock-role.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; + +export class CreateMockRoleDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + description?: string; +} + +export class AddCompetencyDto { + @IsNotEmpty() + @IsNumber() + competencyId: number; +} diff --git a/src/mockModules/mock-role/dto/index.ts b/src/mockModules/mock-role/dto/index.ts new file mode 100644 index 0000000..4f480ba --- /dev/null +++ b/src/mockModules/mock-role/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-mock-role.dto'; +export * from './response-mock-role.dto'; +export * from './update-mock-role.dto'; \ No newline at end of file diff --git a/src/mockModules/mock-role/dto/response-mock-role.dto.ts b/src/mockModules/mock-role/dto/response-mock-role.dto.ts new file mode 100644 index 0000000..b3f4fd3 --- /dev/null +++ b/src/mockModules/mock-role/dto/response-mock-role.dto.ts @@ -0,0 +1,13 @@ +import { ResponseCompetencyDto } from "src/mockModules/mock-competency/dto"; + +export class ResponseMockRoleDto { + readonly id: number; + readonly name: string; + readonly description?: string; + readonly competencies?: ResponseCompetencyDto[]; +} + +export class ResponseAddCompetencyToRoleDto { + readonly role: ResponseMockRoleDto + readonly competency: ResponseCompetencyDto +} \ No newline at end of file diff --git a/src/mockModules/mock-role/dto/update-mock-role.dto.ts b/src/mockModules/mock-role/dto/update-mock-role.dto.ts new file mode 100644 index 0000000..8a75c76 --- /dev/null +++ b/src/mockModules/mock-role/dto/update-mock-role.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateMockRoleDto } from './create-mock-role.dto'; + +export class UpdateMockRoleDto extends PartialType(CreateMockRoleDto) {} \ No newline at end of file diff --git a/src/mockModules/mock-role/mock-role.controller.ts b/src/mockModules/mock-role/mock-role.controller.ts new file mode 100644 index 0000000..fad39bc --- /dev/null +++ b/src/mockModules/mock-role/mock-role.controller.ts @@ -0,0 +1,213 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + ParseIntPipe, + ParseUUIDPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { + AddCompetencyDto, + CreateMockRoleDto, + ResponseAddCompetencyToRoleDto, + ResponseMockRoleDto, + UpdateMockRoleDto, +} from "./dto"; +import { MockRoleService } from "./mock-role.service"; +import { CreateCompetencyDto } from "../mock-competency/dto"; + +@Controller("role") +@ApiTags("mockFracService/role") +export class MockRoleController { + constructor(private roleService: MockRoleService) {} + + @Post() + @ApiOperation({ summary: "create new mock role" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockRoleDto }) + async createRole(@Res() res, @Body() createMockLevelDto: CreateMockRoleDto) { + try { + const role = await this.roleService.createRole(createMockLevelDto); + return res + .status(HttpStatus.OK) + .json({ data: role, message: "Role successfully created." }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get() + @ApiOperation({ summary: "fetch all mock roles" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseMockRoleDto, + isArray: true, + }) + async getAllRoles(@Res() res) { + try { + const roles = await this.roleService.findAllRoles(); + return res + .status(HttpStatus.OK) + .json({ data: roles, message: "Successfully fetched all roles." }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get("formatedRoles") + @ApiOperation({ summary: "fetch all mock roles" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseMockRoleDto, + isArray: true, + }) + async getAllRolesFormated(@Res() res) { + try { + const roles = await this.roleService.findAllRoles(); + return res + .status(HttpStatus.OK) + .json(roles); + + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get("userId/:id") + @ApiOperation({ summary: "fetch all mock roles" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseMockRoleDto, + isArray: true, + }) + async getAllRolesByUserId(@Res() res, @Param("id", ParseUUIDPipe) id: string) { + try { + const roles = await this.roleService.findRolesByUserId(id); + return res + .status(HttpStatus.OK) + .json(roles); + + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get(":id") + @ApiOperation({ summary: "get mock role by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseMockRoleDto }) + async findOne(@Res() res, @Param("id", ParseIntPipe) id: number) { + try { + const role = await this.roleService.findRoleById(id); + return res + .status(HttpStatus.OK) + .json({ data: role, message: "Role successfully fetched." }); + } catch (error) { + return res + .status(error.response.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.response.message ?? "Internal server error." }); + } + } + + @Patch(":id") + @ApiOperation({ summary: "update mock role by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseMockRoleDto }) + async update( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateMockRoleDto: UpdateMockRoleDto + ) { + try { + const roles = await this.roleService.updateRoleById( + id, + updateMockRoleDto + ); + return res + .status(HttpStatus.OK) + .json({ data: roles, message: "Role successfully updated." }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "delete mock role by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseMockRoleDto }) + async remove(@Res() res, @Param("id", ParseIntPipe) id: number) { + try { + const roles = await this.roleService.removeRole(id); + return res + .status(HttpStatus.OK) + .json({ data: roles, message: "Role successfully deleted." }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Post("addExistingCompetencyToRole/:id") + @ApiOperation({ summary: "add competency to role" }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ResponseAddCompetencyToRoleDto, + }) + async addExistingCompetencyToRole( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() competency: AddCompetencyDto + ) { + try { + const role = await this.roleService.addExistingCompetencyToRole( + id, + competency.competencyId + ); + return res.status(HttpStatus.OK).json({ + data: role, + message: `Successfully added competency with id #${competency.competencyId} to Role with id #${id}.`, + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.message }); + } + } + + @Post("addNewCompetencyToRole/:id") + @ApiOperation({ summary: "create and add competency to role" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockRoleDto }) + async addNewCompetencyToRole( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() competency: CreateCompetencyDto + ) { + try { + const connection = await this.roleService.addNewCompetencyToRole( + id, + competency + ); + return res.status(HttpStatus.OK).json({ + data: connection, + message: `Successfully added competency with id # to Role with id #${id}.`, + }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } +} diff --git a/src/mockModules/mock-role/mock-role.module.ts b/src/mockModules/mock-role/mock-role.module.ts new file mode 100644 index 0000000..c037220 --- /dev/null +++ b/src/mockModules/mock-role/mock-role.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MockRoleController } from './mock-role.controller'; +import { MockRoleService } from './mock-role.service'; +import { MockCompetencyService } from '../mock-competency/mock-competency.service'; +import { MockCompetencyLevelService } from '../mock-competency-level/mock-competency-level.service'; + +@Module({ + controllers: [MockRoleController], + providers: [MockRoleService, MockCompetencyService, MockCompetencyLevelService] +}) +export class MockRoleModule {} diff --git a/src/mockModules/mock-role/mock-role.service.ts b/src/mockModules/mock-role/mock-role.service.ts new file mode 100644 index 0000000..894b315 --- /dev/null +++ b/src/mockModules/mock-role/mock-role.service.ts @@ -0,0 +1,249 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; +import { CreateMockRoleDto, UpdateMockRoleDto } from "./dto"; +import { MockCompetencyService } from "../mock-competency/mock-competency.service"; +import { CreateCompetencyDto } from "../mock-competency/dto"; + +@Injectable() +export class MockRoleService { + constructor( + private prisma: PrismaService, + private competency: MockCompetencyService + ) {} + public async createRole(createMockRoleDto: CreateMockRoleDto) { + return this.prisma.role.create({ + data: createMockRoleDto, + }); + } + + public async findAllRoles() { + const roles = await this.prisma.role.findMany({ + select: { + id: true, + name: true, + description: true, + competencies: { + select: { + competency: { + select: { + id: true, + name: true, + competencyLevels: { + select: { + competencyLevel: { + select: { + id: true, + name: true, + levelNumber: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + const transformedRoles = this.transformRolesObject(roles); + + return { roles: transformedRoles }; + } + + public async findRolesByUserId(userId: string) { + const user = await this.prisma.userMetadata.findUnique({ + where: { + userId, + }, + }); + if (!user) { + throw new Error("User data not found."); + } + if (!user.designation) { + throw new Error("User does not have any designation"); + } + + const response = await this.prisma.designation.findUnique({ + where: { + name: user.designation, + }, + select: { + roles: { + select: { + role: { + select: { + id: true, + name: true, + description: true, + competencies: { + select: { + competency: { + select: { + id: true, + name: true, + competencyLevels: { + select: { + competencyLevel: { + select: { + id: true, + name: true, + levelNumber: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!response) { + throw new Error( + `No roles found for the user(designation: ${user.designation}) with id: ${userId}` + ); + } + const roles = response.roles.map((item) => item.role); + const transformedRoles = this.transformRolesObject(roles); + + return { roles: transformedRoles }; + } + + public async findRoleById(id: number) { + const role = await this.prisma.role.findUnique({ + where: { + id, + }, + select: { + id: true, + name: true, + description: true, + competencies: { + select: { + competency: true, + competencyId: true, + }, + }, + }, + }); + if (!role) throw new NotFoundException(`Role with id #${id} not found`); + return role; + } + + public async updateRoleById( + id: number, + updateMockRoleDto: UpdateMockRoleDto + ) { + return this.prisma.role.update({ + where: { + id, + }, + data: updateMockRoleDto, + }); + } + + public async removeRole(id: number) { + return this.prisma.role.delete({ + where: { + id, + }, + }); + } + + public async addExistingCompetencyToRole( + roleId: number, + competencyId: number + ) { + const role = await this.findRoleById(roleId); + + const competency = await this.competency.findCompetencyById(competencyId); + + const connection = await this.prisma.roleToCompetency.create({ + data: { + roleId: role.id, + competencyId: competency.id, + }, + select: { + role: true, + competency: true, + }, + }); + return connection; + } + + public async addNewCompetencyToRole( + roleId: number, + createCompetencyDto: CreateCompetencyDto + ) { + return this.prisma.role.update({ + where: { + id: roleId, + }, + select: { + id: true, + name: true, + description: true, + competencies: { + orderBy: { + competency: { + createdAt: "desc", + }, + }, + select: { + competency: true, + }, + }, + }, + data: { + competencies: { + create: { + competency: { + create: { + ...createCompetencyDto, + }, + }, + }, + }, + }, + }); + } + + public async getCompetenciesByRoleId(id: number) { + return this.prisma.roleToCompetency.findMany({ + where: { + roleId: id, + }, + select: { + competencyId: true, + }, + }); + } + + public transformRolesObject(roles) { + const transformedRoles: any = []; + roles.map(async (role) => { + const transformedRole = { + id: role.id, + name: role.name, + description: role.description, + competency: role.competencies.map((competency) => ({ + id: competency.competency.id, + name: competency.competency.name, + levels: competency.competency.competencyLevels.map((level) => ({ + id: level.competencyLevel.id, + name: level.competencyLevel.name, + levelNumber: level.competencyLevel.levelNumber, + })), + })), + }; + transformedRoles.push(transformedRole); + }); + + return transformedRoles; + } +} diff --git a/src/mockModules/mock-user/dto/create-mock-user.dto.ts b/src/mockModules/mock-user/dto/create-mock-user.dto.ts new file mode 100644 index 0000000..6a230a5 --- /dev/null +++ b/src/mockModules/mock-user/dto/create-mock-user.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { UserRolesEnum } from "@prisma/client"; +import { + IsDate, + IsEmail, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsStrongPassword, +} from "class-validator"; + +export class CreateMockUserDto { + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ enum: UserRolesEnum, example: "CONSUMER" }) + @IsEnum(UserRolesEnum, { each: true, always: true }) + @IsNotEmpty() + role: UserRolesEnum; + + @IsNotEmpty() + @IsString() + userName: string; + + @IsStrongPassword() + @IsString() + password: string; + + @IsOptional() + @IsNotEmpty() + @IsString() + profilePicture: string; + + @IsNotEmpty() + @IsNumber() + levelNumber: number; + + @IsNotEmpty() + @IsString() + designation: string; + + // Optional creation date of the request. + @IsDate() + @IsOptional() + createdAt?: Date; + + // Optional update date of the request. + @IsDate() + @IsOptional() + updatedAt?: Date; +} diff --git a/src/mockModules/mock-user/dto/response-mock-user.dto.ts b/src/mockModules/mock-user/dto/response-mock-user.dto.ts new file mode 100644 index 0000000..feb0823 --- /dev/null +++ b/src/mockModules/mock-user/dto/response-mock-user.dto.ts @@ -0,0 +1,11 @@ + +export class ResponseMockUserDto { + readonly id: string; + readonly email: string; + readonly role: string; + readonly userName: string; + readonly profilePicture?: string; + readonly designation: String; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} diff --git a/src/mockModules/mock-user/dto/update-mock-user.dto.ts b/src/mockModules/mock-user/dto/update-mock-user.dto.ts new file mode 100644 index 0000000..cb4c624 --- /dev/null +++ b/src/mockModules/mock-user/dto/update-mock-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateMockUserDto } from './create-mock-user.dto'; + +export class UpdateMockUserDto extends PartialType(CreateMockUserDto) {} diff --git a/src/mockModules/mock-user/mock-user.controller.ts b/src/mockModules/mock-user/mock-user.controller.ts new file mode 100644 index 0000000..3ba8c28 --- /dev/null +++ b/src/mockModules/mock-user/mock-user.controller.ts @@ -0,0 +1,110 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { CreateMockUserDto } from "./dto/create-mock-user.dto"; +import { ResponseMockUserDto } from "./dto/response-mock-user.dto"; +import { UpdateMockUserDto } from "./dto/update-mock-user.dto"; +import { MockUserService } from "./mock-user.service"; + +@Controller("user") +@ApiTags("mockFracService/user") +export class MockUserController { + constructor(private readonly mockUserService: MockUserService) {} + + @Post() + @ApiOperation({ summary: "create new mock user" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockUserDto }) + async create(@Body() createMockUserDto: CreateMockUserDto, @Res() res) { + try { + const user = await this.mockUserService.create(createMockUserDto); + return res + .status(HttpStatus.OK) + .json({ data: user, message: "Successfully done" }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error?.meta?.cause || `Failed to create user` }); + } + } + + @Get() + @ApiOperation({ summary: "get all mock user" }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ResponseMockUserDto, + isArray: true, + }) + async findAll(@Res() res) { + try { + const user = await this.mockUserService.findAll(); + return res + .status(HttpStatus.OK) + .json({ data: user, message: "Successfully done" }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Get(":userId") + @ApiOperation({ summary: "get mock user by userId" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockUserDto }) + async findOne(@Param("userId") userId: string, @Res() res) { + try { + const user = await this.mockUserService.findOne(userId); + return res + .status(HttpStatus.OK) + .json({ data: user, message: "Successfully done" }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.message }); + } + } + + @Patch(":userId") + @ApiOperation({ summary: "update mock user by userId" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockUserDto }) + async update( + @Param("userId") userId: string, + @Body() updateMockUserDto: UpdateMockUserDto, + @Res() res + ) { + try { + const user = await this.mockUserService.update(userId, updateMockUserDto); + return res + .status(HttpStatus.OK) + .json({ data: user, message: "Successfully done" }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } + + @Delete(":userId") + @ApiOperation({ summary: "delete mock user by userId" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseMockUserDto }) + async remove(@Param("userId") userId: string, @Res() res) { + try { + const user = await this.mockUserService.remove(userId); + return res + .status(HttpStatus.OK) + .json({ data: user, message: "Successfully done" }); + } catch (error) { + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.meta.cause }); + } + } +} diff --git a/src/mockModules/mock-user/mock-user.module.ts b/src/mockModules/mock-user/mock-user.module.ts new file mode 100644 index 0000000..d4f9c58 --- /dev/null +++ b/src/mockModules/mock-user/mock-user.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { MockUserService } from "./mock-user.service"; +import { MockUserController } from "./mock-user.controller"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Module({ + controllers: [MockUserController], + providers: [MockUserService], + exports: [MockUserService], +}) +export class MockUserModule {} diff --git a/src/mockModules/mock-user/mock-user.service.ts b/src/mockModules/mock-user/mock-user.service.ts new file mode 100644 index 0000000..f311d23 --- /dev/null +++ b/src/mockModules/mock-user/mock-user.service.ts @@ -0,0 +1,77 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { CreateMockUserDto } from "./dto/create-mock-user.dto"; +import { UpdateMockUserDto } from "./dto/update-mock-user.dto"; +import { PrismaService } from "../../prisma/prisma.service"; + +@Injectable() +export class MockUserService { + constructor(private prisma: PrismaService) {} + + public async create(createMockUserDto: CreateMockUserDto) { + return await this.prisma.user.create({ + data: createMockUserDto, + select: { + ...this.userSelectObj, + }, + }); + } + + public async findAll() { + return await this.prisma.user.findMany({ + select: { + ...this.userSelectObj, + }, + }); + } + + public async findOne(id: string) { + const user = await this.prisma.user.findUnique({ + where: { + id, + }, + select: { + ...this.userSelectObj, + }, + }); + if (!user) throw new NotFoundException(`User with id #${id} not found`); + return user; + } + + public async update(id: string, updateMockUserDto: UpdateMockUserDto) { + return await this.prisma.user.update({ + where: { + id, + }, + select: { + ...this.userSelectObj, + }, + data: updateMockUserDto, + }); + } + + public async remove(id: string) { + return await this.prisma.user.delete({ + where: { + id, + }, + select: { + ...this.userSelectObj, + }, + }); + } + + userSelectObj = { + id: true, + email: true, + role: true, + userName: true, + profilePicture: true, + designation:true, + // Level: { + // select: { + // levelNumber : true + // }, + createdAt: true, + updatedAt: true, + }; +} diff --git a/src/mockModules/mock.module.ts b/src/mockModules/mock.module.ts new file mode 100644 index 0000000..5a0c749 --- /dev/null +++ b/src/mockModules/mock.module.ts @@ -0,0 +1,25 @@ +import { Module } from "@nestjs/common"; +import { MockRoleModule } from "./mock-role/mock-role.module"; +import { MockCompetencyModule } from "./mock-competency/mock-competency.module"; +import { MockCompetencyLevelModule } from "./mock-competency-level/mock-competency-level.module"; +import { RouterModule } from "@nestjs/core"; +import { MockUserModule } from "./mock-user/mock-user.module"; +import { MockDesignationModule } from './mock-designation/mock-designation.module'; + +@Module({ + imports: [ + MockRoleModule, + MockCompetencyModule, + MockCompetencyLevelModule, + MockUserModule, + MockDesignationModule, + RouterModule.register([ + { path: "mockFracService/", module: MockRoleModule }, + { path: "mockFracService/", module: MockCompetencyModule }, + { path: "mockFracService/", module: MockCompetencyLevelModule }, + { path: "mockFracService/", module: MockUserModule }, + { path: "mockFracService/", module: MockDesignationModule }, + ]), + ], +}) +export class MockFracModule {} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts index e6d1e9e..e03c438 100644 --- a/src/prisma/prisma.module.ts +++ b/src/prisma/prisma.module.ts @@ -1,4 +1,10 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; +import { ConfigService } from "@nestjs/config"; -@Module({}) +@Global() +@Module({ + providers: [PrismaService, ConfigService], + exports: [PrismaService], +}) export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 5a993ff..aa0a5d6 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,5 +1,16 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { PrismaClient } from "@prisma/client"; @Injectable() -export class PrismaService extends PrismaClient{} +export class PrismaService extends PrismaClient { + constructor(private config: ConfigService) { + super({ + datasources: { + db: { + url: config.get("DATABASE_URL"), + }, + }, + }); + } +} diff --git a/src/question-bank/dto/create-question-bank.dto.ts b/src/question-bank/dto/create-question-bank.dto.ts new file mode 100644 index 0000000..dbaf39b --- /dev/null +++ b/src/question-bank/dto/create-question-bank.dto.ts @@ -0,0 +1,90 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + ArrayNotEmpty, + IsArray, + IsInt, + IsNotEmpty, + IsOptional, + IsString, +} from "class-validator"; +import { UpdateQuestionBankDto2 } from "./update-question-bank.dto"; + +// CreateQuestionBankDto is used for creating a new question bank +export class CreateQuestionBankDto { + // Compentency id associated with the question bank + @IsNotEmpty() + @IsInt() + competencyId: number; + + // Question for the question bank + @IsNotEmpty() + @IsString() + question: string; + + // Competency Level number for the question bank + @IsNotEmpty() + @IsInt() + competencyLevelNumber: number; +} +export class CreateFileUploadDto { + // Competency associated with Question bank csv file + @IsNotEmpty() + @IsString() + competency: string; + + // competencyLevel number of the uploaded question bank csv file + @IsNotEmpty() + @IsInt() + competencyLevelNumber: number; + + // Questions of the uploaded question bank csv file + @IsNotEmpty() + @IsString() + question: string; +} + +// Question bank filter +export class QuestionBankFilterDto { + // Optional competencyId filter, validated that it's valid compentency id or not + @IsOptional() + @IsInt() + competencyId?: number; + + // Optional competencyLevelNumber filter, validate that it's valid competency Level number or not + @IsOptional() + @IsInt() + competencyLevelNumber?: number; + + // Optional limit for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + limit?: number; + + // Optional offset for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + offset?: number; + + // Optional field to specify the order of results, validate that it's a string. + @IsOptional() + @IsString() + orderBy?: string; +} + +export class CreateUpdateDeleteQuesitonsDto { + @IsOptional() + @IsNotEmpty() + createQuestions?: CreateQuestionBankDto[]; + + @IsOptional() + @IsNotEmpty() + updateQuestions?: UpdateQuestionBankDto2[] + + @ApiProperty({isArray: true, example:[1, 2, 3]}) + @IsOptional() + @IsNotEmpty() + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + deleteQuestions?: number[]; +} \ No newline at end of file diff --git a/src/question-bank/dto/response-question-bank.dto.ts b/src/question-bank/dto/response-question-bank.dto.ts new file mode 100644 index 0000000..fcf52cf --- /dev/null +++ b/src/question-bank/dto/response-question-bank.dto.ts @@ -0,0 +1,7 @@ +export class ResponseQuestionBankDto { + readonly competencyId: number; + readonly competencyLevelNumber: number; + readonly question: string; + readonly createdAt: Date; + readonly updatedAt: Date; +} diff --git a/src/question-bank/dto/update-question-bank.dto.ts b/src/question-bank/dto/update-question-bank.dto.ts new file mode 100644 index 0000000..0d76229 --- /dev/null +++ b/src/question-bank/dto/update-question-bank.dto.ts @@ -0,0 +1,19 @@ +import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class UpdateQuestionBankDto { + // Question for the question bank + @IsNotEmpty() + @IsString() + question: string; +} + +export class UpdateQuestionBankDto2 { + @IsNotEmpty() + @IsInt() + questionId: number + + // Question for the question bank + @IsNotEmpty() + @IsString() + question: string; +} \ No newline at end of file diff --git a/src/question-bank/question-bank.controller.spec.ts b/src/question-bank/question-bank.controller.spec.ts new file mode 100644 index 0000000..4b2a255 --- /dev/null +++ b/src/question-bank/question-bank.controller.spec.ts @@ -0,0 +1,149 @@ +import * as pactum from "pactum"; +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import { QuestionBankModule } from "./question-bank.module"; +import { CreateQuestionBankDto } from "./dto/create-question-bank.dto"; +import { UpdateQuestionBankDto } from "./dto/update-question-bank.dto"; + +describe("QuestionBankController", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [QuestionBankModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + afterAll(async () => { + await app.close(); + }); + + describe("QuestionBankController createQuestionByCompentencyLevel()", () => { + it("should return created question by compentency level", async () => { + const createQuestionDto: CreateQuestionBankDto = { + competencyId: 1, + question: "This is test question.", + competencyLevelNumber: 1, + }; + const response = await pactum + .spec() + .post("/question-bank") + .withBody(createQuestionDto) + .expectStatus(201); + const createdQuestion = JSON.parse(JSON.stringify(response.body)); + + expect(createdQuestion.message).toEqual("Question created sucessfully."); + expect(createdQuestion).not.toBeNull(); + expect(createdQuestion.data.competencyId).toEqual( + createQuestionDto.competencyId + ); + expect(createdQuestion.data.question).toEqual(createQuestionDto.question); + expect(createdQuestion.data.competencyLevelNumber).toEqual( + createQuestionDto.competencyLevelNumber + ); + }); + }); + + describe("QuestionBankController getAllQuestions()", () => { + it("should return all questions", async () => { + const response = await pactum + .spec() + .get("/question-bank") + .expectStatus(200); + + const questions = JSON.parse(JSON.stringify(response.body)); + expect(questions.message).toEqual("Successfully fetched all questions."); + expect(questions.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("QuestionBankController updateQuestionById()", () => { + const testData = { + id: 1, + }; + it("should update the question by id", async () => { + const updatedQuestionDto: UpdateQuestionBankDto = { + question: "Updated Test Question", + }; + const response = await pactum + .spec() + .patch(`/question-bank/update/${testData.id}`) + .withPathParams({ + id: testData.id, + }) + .withBody(updatedQuestionDto) + .expectStatus(200); + const updatedQuestion = JSON.parse(JSON.stringify(response.body)); + expect(updatedQuestion.message).toEqual( + `Successfully updated question for id #${testData.id}` + ); + expect(updatedQuestion.data.question).toEqual( + updatedQuestionDto.question + ); + expect(updatedQuestion.data.competencyId).toBeDefined(); + expect(updatedQuestion.data.competencyLevelNumber).toBeDefined(); + }); + }); + + describe("QuestionBankController deleteQuestionById()", () => { + const testData = { + id: 1, + }; + it("should delete a question with given id", async () => { + const response = await pactum + .spec() + .delete(`/question-bank/delete/${testData.id}`) + .withPathParams({ + id: testData.id, + }) + .expectStatus(200); + const deletedQuestion = JSON.parse(JSON.stringify(response.body)); + expect(deletedQuestion.message).toEqual( + `Successfully deleted question for id #${testData.id}` + ); + expect(deletedQuestion.data.question).toBeDefined(); + expect(deletedQuestion.data.competencyId).toBeDefined(); + expect(deletedQuestion.data.competencyLevelNumber).toBeDefined(); + }); + }); + + describe("QuestionBankController getAllQuestionsForUser()", () => { + const testData = { + userId: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", + }; + it("should get all the mapped question for a user with given userId.", async () => { + const response = await pactum + .spec() + .get(`/question-bank/user/${testData.userId}`) + .withPathParams({ + userId: testData.userId, + }) + .expectStatus(200); + const questionsForUser = JSON.parse(JSON.stringify(response.body)); + expect(questionsForUser.message).toEqual( + `Successfully get all the question for id #${testData.userId}` + ); + expect(questionsForUser.data.length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/src/question-bank/question-bank.controller.ts b/src/question-bank/question-bank.controller.ts new file mode 100644 index 0000000..c18c509 --- /dev/null +++ b/src/question-bank/question-bank.controller.ts @@ -0,0 +1,333 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Logger, + HttpStatus, + Res, + Query, + ParseIntPipe, + UseInterceptors, + UploadedFile, + ParseUUIDPipe, +} from "@nestjs/common"; +import { QuestionBankService } from "./question-bank.service"; +import { + CreateQuestionBankDto, + CreateUpdateDeleteQuesitonsDto, + QuestionBankFilterDto, +} from "./dto/create-question-bank.dto"; +import { UpdateQuestionBankDto } from "./dto/update-question-bank.dto"; +import { + ApiBody, + ApiConsumes, + ApiOperation, + ApiResponse, + ApiTags, +} from "@nestjs/swagger"; +import { ResponseQuestionBankDto } from "./dto/response-question-bank.dto"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { multerOptions } from "../config/multer-options.config"; + +@Controller("question-bank") +@ApiTags("question-bank") +export class QuestionBankController { + // Create a logger instance for this controller to log events and errors. + private readonly logger = new Logger(QuestionBankController.name); + + // The question bank controller class, injecting the question bank service. + constructor(private readonly questionBankService: QuestionBankService) {} + + // Api for creating a new question + @Post() + @ApiOperation({ summary: "Create a new question" }) // Api operation for the swagger + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseQuestionBankDto }) // Api response for the swagger + async createQuestionByCompentencyLevel( + @Res() res, + @Body() createQuestionBankDto: CreateQuestionBankDto + ): Promise { + try { + // Log the initiation for the question creation + this.logger.log(`Initiate to create a question`); + + const createdQuestion = + await this.questionBankService.createQuestionByCompentencyId( + createQuestionBankDto + ); + // Log the successful creation of the question bank + this.logger.log(`Successfully created question`); + return res.status(HttpStatus.CREATED).json({ + message: "Question created sucessfully.", + data: createdQuestion, + }); + } catch (error) { + this.logger.error(`Failed to create new question.`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create new question.`, + }); + } + } + + // Api for getting all questions or by filtering using competencyId or competencyLevelId + @Get() + @ApiOperation({ summary: "Get all the questions" }) // Api operation for the swagger + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseQuestionBankDto, + isArray: true, + }) // Api response for the swagger + async getAllQuestions( + @Res() res, + @Query() filter: QuestionBankFilterDto + ): Promise { + try { + // Log the initiation for fetching all the questions + this.logger.log(`Initiated fetching all the questions.`); + const questions = await this.questionBankService.getAllQuestions(filter); + + // Log the successful fetching of data from db. + this.logger.log(`Successfully fetched all questions`); + + // return the successful response for fetching the data from the db. + return res.status(HttpStatus.OK).json({ + message: "Successfully fetched all questions.", + data: questions, + }); + } catch (error) { + // log the error message + this.logger.error(`Failed to get all questions.`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to get all questions.`, + }); + } + } + + // Api to update the question by Id + @Patch("update/:id") + @ApiOperation({ summary: "Update the question by Id" }) // Api operation for the swagger + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseQuestionBankDto, + }) // Api operation for the swagger + async updateQuestionById( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateQuestionBankDto: UpdateQuestionBankDto + ): Promise { + try { + // Log the initiation for updating the question for the given id + this.logger.log(`Initiated updating the question for id #${id}`); + // Update the question for the id + const updateQuestion = await this.questionBankService.updateQuestionById( + id, + updateQuestionBankDto + ); + // Log the successful update of the question with the given id + this.logger.log(`Successfully updated question for id #${id}`); + // Return response and statuscode for the successful update of the question + return res.status(HttpStatus.OK).json({ + message: `Successfully updated question for id #${id}`, + data: updateQuestion, + }); + } catch (error) { + // Log the error + this.logger.error(`Failed to update question for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + // Return the response and status code for the failed update of the question + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to update question for id #${id}`, + }); + } + } + + // Delete the Question for the id + @Delete("/delete/:id") + @ApiOperation({ summary: "Delete the question by Id" }) // Api operation for swagger + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseQuestionBankDto, + }) + async deleteQuestionById( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiating deleting of a question with an id ${id}`); + + const deletedQuestion = await this.questionBankService.deleteQuestionById( + id + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully deleted question for id #${id}`, + data: deletedQuestion, + }); + } catch (error) { + this.logger.error(`Failed to delete question for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to delete question for id #${id}`, + }); + } + } + // API to upload csv file question bank + @Post("upload") + @UseInterceptors(FileInterceptor("file", multerOptions)) + @ApiConsumes("multipart/form-data") // Specify content type as multipart/form-data + @ApiBody({ + schema: { + type: "object", + properties: { + file: { + type: "string" || "number", + format: "binary", + }, + }, + }, + }) + async uploadQuestions(@UploadedFile() file, @Res() res) { + // Log the initiaton for csv file upload + this.logger.log(`Initiate to upload a csv file.`); + try { + await this.questionBankService.uploadCsvFile(file.path); + this.logger.log(`Successfully uploaded question bank.`); + return res.status(HttpStatus.CREATED).json({ + message: "Question bank uploaded sucessfully.", + }); + } catch (error) { + this.logger.error(`Failed to upload question bank.`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to upload question bank.`, + }); + } + } + + // Get all the mapped questions for the user + @Get("user/:id") + @ApiOperation({ + summary: "Get all the questions for a user in the survey form.", + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseQuestionBankDto, + isArray: true, + }) + async getAllQuestionsForUser( + @Res() res, + @Param("id", ParseUUIDPipe) id: string + ): Promise { + try { + this.logger.log( + `Initiating the fetching of all the question for user with id#${id}` + ); + + const getAllQuestionsForUser = + await this.questionBankService.getAllQuestionsForUser(id); + + return res.status(HttpStatus.OK).json({ + message: `Successfully get all the question for id #${id}`, + data: getAllQuestionsForUser, + }); + } catch (error) { + this.logger.error(`Failed to get all questions for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to get all questions for id #${id}`, + }); + } + } + + //API to Create, Update and Delete Multiple Questions + @Post("/updateMultipleQuestions") + @ApiOperation({ summary: "Create, Update and Delete Multiple Questions" }) + @ApiResponse({ status: HttpStatus.OK }) + async createUpdateDeleteQuesitons( + @Res() res, + @Body() createUpdateDeleteQuesitonsDto: CreateUpdateDeleteQuesitonsDto + ) { + // Log the initiaton for csv file upload + this.logger.log(`Initiate updating the question bank.`); + try { + await this.questionBankService.createUpdateDeleteQuesitons( + createUpdateDeleteQuesitonsDto + ); + this.logger.log(`Successfully updated the question bank.`); + return res.status(HttpStatus.CREATED).json({ + message: "Question bank updated sucessfully.", + }); + } catch (error) { + this.logger.error(`Failed to update question bank.`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to update question bank.`, + }); + } + } + + // API to get question bank template header + @Get("/template") + @ApiOperation({ summary: "get question bank template" }) + @ApiResponse({ status: HttpStatus.OK }) + async getQuestionBankTemplate( + @Res() res + ) { + this.logger.log(`Initiate to fetch the question bank template.`); + try { + const header = await this.questionBankService.getQuestionBankTemplate(); + this.logger.log( + `Successfully fetch the question bank template.` + ); + return res.status(HttpStatus.CREATED).json({ + message: "Successfully fetch the question bank template.", + data : header + }); + } catch (error) { + this.logger.error(`Failed to get the question bank template.`, error); + // get error message and status code + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to get the question bank template.`, + }); + } + } +} diff --git a/src/question-bank/question-bank.module.ts b/src/question-bank/question-bank.module.ts new file mode 100644 index 0000000..5b02971 --- /dev/null +++ b/src/question-bank/question-bank.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { QuestionBankService } from './question-bank.service'; +import { QuestionBankController } from './question-bank.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { MockUserService } from '../mockModules/mock-user/mock-user.service'; +import { MockDesignationService } from '../mockModules/mock-designation/mock-designation.service'; +import { MockRoleService } from '../mockModules/mock-role/mock-role.service'; +import { MockCompetencyService } from '../mockModules/mock-competency/mock-competency.service'; +import { MockCompetencyLevelService } from '../mockModules/mock-competency-level/mock-competency-level.service'; +import { FileUploadService } from '../file-upload/file-upload.service'; +import { TarentoService } from 'src/external-services/tarento/tarento.service'; + +@Module({ + // Import the PrismaModule to make Prisma service available within this module + imports: [PrismaModule], + + // Declare the QuestionBankController as a controller for this module + controllers: [QuestionBankController], + + // Declare the QuestionBankService and PrismaService as providers for this module + providers: [ + QuestionBankService, + MockCompetencyService, + MockCompetencyLevelService, + MockUserService, + MockDesignationService, + MockRoleService, + FileUploadService, + TarentoService + ], + + // Export the QuestionBankService to make it available for other modules that import this module + exports: [QuestionBankService], +}) +export class QuestionBankModule {} diff --git a/src/question-bank/question-bank.service.ts b/src/question-bank/question-bank.service.ts new file mode 100644 index 0000000..2b5130e --- /dev/null +++ b/src/question-bank/question-bank.service.ts @@ -0,0 +1,450 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import _ from "lodash"; +import { + CreateFileUploadDto, + CreateQuestionBankDto, + CreateUpdateDeleteQuesitonsDto, + QuestionBankFilterDto, +} from "./dto/create-question-bank.dto"; +import { UpdateQuestionBankDto } from "./dto/update-question-bank.dto"; +import { PrismaService } from "../prisma/prisma.service"; +import { MockCompetencyService } from "../mockModules/mock-competency/mock-competency.service"; +import { MockUserService } from "../mockModules/mock-user/mock-user.service"; +import { MockRoleService } from "./../mockModules/mock-role/mock-role.service"; +import { MockDesignationService } from "../mockModules/mock-designation/mock-designation.service"; +import { FileUploadService } from "../file-upload/file-upload.service"; +import axios from "axios"; +import { TarentoService } from "src/external-services/tarento/tarento.service"; + +@Injectable() +export class QuestionBankService { + constructor( + private prisma: PrismaService, + private competencyService: MockCompetencyService, + private mockUserService: MockUserService, + private mockDesignationService: MockDesignationService, + private mockRoleService: MockRoleService, + private fileUploadService: FileUploadService, + private tarentoService: TarentoService + ) {} + + async createQuestionByCompentencyId( + createQuestionBankDto: CreateQuestionBankDto + ) { + // check for compentencyId exist in competency model in db + const checkCompentencyId = await this.prisma.competency.findUnique({ + where: { + id: createQuestionBankDto.competencyId, + }, + }); + + // if not found throw error + if (!checkCompentencyId) { + throw new NotFoundException( + "Either Competency or Competency Level is not Found" + ); + } + // Otherwise create a new question bank + const newQuestion = await this.prisma.questionBank.create({ + data: createQuestionBankDto, + }); + return newQuestion; + } + + async getAllQuestions(filter: QuestionBankFilterDto) { + // Get all questions and filter using compentencyId and compentencyLevelId + const { competencyId, competencyLevelNumber, limit, offset, orderBy } = + filter; + return this.prisma.questionBank.findMany({ + where: { + competencyId: competencyId ?? undefined, // Optional compentencyId filter + competencyLevelNumber: competencyLevelNumber ?? undefined, // Optional competencyLevelNumber + }, + orderBy: { + [orderBy || "createdAt"]: "asc", // Default sorting by createdAt + }, + skip: offset ?? undefined, + take: limit ?? undefined, + }); + } + + async updateQuestionById( + questionId: number, + updateQuestionBankDto: UpdateQuestionBankDto + ) { + // Check if there is question in db for the questionId + const findQuestion = await this.prisma.questionBank.findUnique({ + where: { + id: questionId, + }, + }); + // If not found throw error message + if (!findQuestion) { + throw new NotFoundException( + `No question exists with the id #${questionId}.` + ); + } + // Otherwise update it's details + return this.prisma.questionBank.update({ + where: { + id: questionId, + }, + data: updateQuestionBankDto, + }); + } + + async deleteQuestionById(questionId: number) { + // Check the question is there in db for the given questionId and competencyLevelId + const findQuestion = await this.prisma.questionBank.findUnique({ + where: { + id: questionId, + }, + }); + // If not found throw error message + if (!findQuestion) { + throw new NotFoundException( + `No question exists with the Id #${questionId}.` + ); + } + // Otherwise delete question related to that competency id + return this.prisma.questionBank.delete({ + where: { + id: questionId, + }, + }); + } + + public async uploadCsvFile(filepath) { + try { + // Parsed the uploaded data + let parsedData; + parsedData = await this.fileUploadService.parseCSV(filepath); + // Store the parsedData in the db + await this.bulkUploadQuestions(parsedData); + } catch (error) { + await this.fileUploadService.deleteUploadedFile(filepath); + throw error; + } + // Clean up after the sucessful upload of csv data + await this.fileUploadService.deleteUploadedFile(filepath); + } + + public async bulkUploadQuestions(data: CreateFileUploadDto[]) { + // check the header from the data + const headers = Object.keys(data[0]); + const requiredHeaders = ["competency", "competencyLevelNumber", "question"]; + const nonValidHeader = headers.filter( + (item) => !requiredHeaders.includes(item) + ); + if (nonValidHeader.length) { + throw new BadRequestException(`Invalid columns ${nonValidHeader}`); + } + // Check if the competency Existed or not the given csv data if not then filter and throw error + const allCompetencies = await this.competencyService.findAllCompetencies(); + + // Extract an array of competency names from the fetched competencies + const competencyNames = allCompetencies.map( + (competency) => competency.name + ); + + // check for the data that is not in the competency model + const dataNotInCompetencyModel = data.filter( + (item) => !competencyNames.includes(item["competency"]) + ); + + // If there is data not in the competency model, you can throw an error or handle it as needed. + if (dataNotInCompetencyModel.length > 0) { + throw new Error( + `Some data is not in the competency model #${dataNotInCompetencyModel + .map((item) => item.competency) + .join(",")}` + ); + } + + // Check for the non-numeric competencyLevelNumber which is not a number in the csv data + const dataWithNonNumericCompetencyLevelNumber = data.filter((item) => { + const competencyLevelNumber = Number(item.competencyLevelNumber); + return ( + isNaN(competencyLevelNumber) || + competencyLevelNumber < 1 || + competencyLevelNumber > 7 + ); // Check if it's not a number and value should b/w 1 to 7 + }); + // Throw an error for the dataWithNonNumericCompetencyLevelNumber + if (dataWithNonNumericCompetencyLevelNumber.length > 0) { + throw new Error( + `The following competency level numbers are not numeric : ${dataWithNonNumericCompetencyLevelNumber + .map( + (item) => + `Competency ${item?.competency} with competency value #${ + item?.competencyLevelNumber || "Empty" + }.` + ) + .join(",")}` + ); + } + + // Filter for the empty question in the csv data + const dataEmptyQuestion = data.filter((item) => !item.question); + // Throw an error for the dataEmptyQuestion + if (dataEmptyQuestion.length > 0) { + throw new Error( + `The following questions are empty: ${dataEmptyQuestion + .map( + (item) => + `Competency ${item?.competency} with competency value#${item?.competencyLevelNumber}.` + ) + .join(",")}` + ); + } + + const createManyDataPromises: Promise[] = data.map(async (item) => { + const getCompetency = await this.competencyService.findCompetencyByName( + item.competency + ); + if (getCompetency && getCompetency.id) { + // Check if a question with the same competencyId exists in the database + const existingQuestion = await this.prisma.questionBank.findFirst({ + where: { + competencyId: getCompetency.id, + competencyLevelNumber: Number(item["competencyLevelNumber"]), + }, + }); + if (existingQuestion) { + // If it exists, update the existing question + await this.prisma.questionBank.update({ + where: { + id: existingQuestion.id, + }, + data: { + question: item["question"], + competencyLevelNumber: Number(item["competencyLevelNumber"]), + }, + }); + } else { + // If it doesn't exist, create a new entry + await this.createQuestionByCompentencyId({ + question: item["question"], + competencyId: getCompetency.id, + competencyLevelNumber: Number(item["competencyLevelNumber"]), + }); + } + } else { + throw new Error("Competency Id not found"); + } + }); + try { + await Promise.all(createManyDataPromises); + } catch (error) { + console.error("Error storing data in the database:", error); + throw new Error("Failed to store data in the database"); + } + } + + async getAllQuestionsForUser(userId: string) { + // const user = await this.mockUserService.findOne(userId); + + let response = await this.tarentoService.getUser(userId); + + const user = response.data.result?.response?.content[0]?.profileDetails?.professionalDetails[0]; + if (!user) { + // Handle the case when the user is not found. + throw new Error("User not found"); + } + + // Get the user's designation + const designation = user?.designation; + + // Get all the roles for a designation + const userRoles = + await this.mockDesignationService.findAllRolesForDesignation(designation); + + // Get roleId of each role of the user + const roles = userRoles?.Roles.map((item) => { + return { + id: item.id, + name: item.name, + }; + }); + + const competenciesForRole = await Promise.all( + roles?.map(async (role) => { + const competency = await this.mockRoleService.getCompetenciesByRoleId( + role.id + ); + return competency; + }) || [] + ); + + // Flatten the array and extract unique competencyIds + const uniqueCompetencyIds = Array.from( + new Set(competenciesForRole.flat().map((comp) => comp.competencyId)) + ); + + const questionLists: any = []; + if (uniqueCompetencyIds?.length) { + for (let i = 0; i < uniqueCompetencyIds.length; i++) { + const questions = await this.prisma.questionBank.findMany({ + where: { + competencyId: uniqueCompetencyIds[i], + }, + select: { + id: true, + question: true, + }, + }); + + const mappedQuestions = _.map(questions, (data) => { + const { id, question } = data; + return { + question, + questionId: id, + }; + }); + questionLists.push(...mappedQuestions); + } + } + if (!questionLists.length) { + throw new NotFoundException(`No Question Found for user with #${userId}`); + } + return questionLists; + } + + public async getQuestionById(id: number) { + const question = await this.prisma.questionBank.findUnique({ + where: { id }, + }); + + if (!question) + throw new NotFoundException(`question with id #${id} not found`); + return question; + } + + public async createUpdateDeleteQuesitons( + questionBank: CreateUpdateDeleteQuesitonsDto + ) { + const { createQuestions, updateQuestions, deleteQuestions } = questionBank; + await this.prisma.$transaction(async (prismaClient) => { + if (!_.isUndefined(deleteQuestions) && !_.isEmpty(deleteQuestions)) { + for (const deleteQuestionId of deleteQuestions) { + await prismaClient.questionBank.delete({ + where: { + id: deleteQuestionId, + }, + }); + } + } + + if (!_.isUndefined(updateQuestions) && !_.isEmpty(updateQuestions)) { + for (const updateQuestion of updateQuestions) { + await prismaClient.questionBank.update({ + where: { + id: updateQuestion.questionId, + }, + data: { + question: updateQuestion.question, + }, + }); + } + } + + if (!_.isUndefined(createQuestions) && !_.isEmpty(createQuestions)) { + for (const createQuestion of createQuestions) { + const checkCompentencyId = await prismaClient.competency.findUnique({ + where: { + id: createQuestion.competencyId, + }, + }); + + if (!checkCompentencyId) { + throw new NotFoundException("Competency is not Found"); + } + + await prismaClient.questionBank.create({ + data: { + competencyId: createQuestion.competencyId, + competencyLevelNumber: createQuestion.competencyLevelNumber, + question: createQuestion.question, + }, + }); + } + } + }); + } + + public async getUnmappedCompetency() { + const QuestionsMappedWithCompetency = + await this.prisma.questionBank.findMany(); + // console.log("QuestionsMappedWithCompetency", QuestionsMappedWithCompetency); + const mappedCompetenciesSet = new Set( + QuestionsMappedWithCompetency.map( + (mapped) => `${mapped.competencyId}_${mapped.competencyLevelNumber}` + ) + ); + // get all competencies + const allCompetencies = + await this.competencyService.findAllCompetenciesWithLevelNames(); + + const unmappedCompetencies: { + competencyId: number; + competencyLevelNumber: number; + }[] = []; + + allCompetencies.forEach((competency) => { + const { competencyLevels } = competency; + competencyLevels.forEach(async (level) => { + const key = `${level.competencyId}_${level.competencyLevel.levelNumber}`; + + if (!mappedCompetenciesSet.has(key)) { + unmappedCompetencies.push({ + competencyId: level.competencyId, + competencyLevelNumber: level.competencyLevel.levelNumber, + }); + } + }); + }); + const data: any = []; + // Use map to create an array of promises + const promises = unmappedCompetencies.map(async (item) => { + const competency = ( + await this.competencyService.findCompetencyById(item.competencyId) + ).name; + const competencyAndLevel = { + competency, + competencyLevelNumber: item.competencyLevelNumber, + }; + return competencyAndLevel; // Return the value to the map function + }); + + try { + await Promise.all(promises) + .then((result) => { + // All asynchronous operations have completed + data.push(...result); // Push all results to the data array + }) + .catch((error) => { + // Handle errors if any of the promises reject + console.error(error); + }); + } catch (error) { + console.error("Error storing data in the database:", error); + throw new Error("Failed to store data in the database"); + } + return data; + } + + public async getQuestionBankTemplate() { + const header = ["competency", "competencyLevelNumber", "question"]; + // get all unmapped competency and their competencyLevelNumber for which there + // is no any question associated with it + const unmappedCompetencies = await this.getUnmappedCompetency(); + return { + header, + unmappedCompetencies, + }; + } +} diff --git a/src/response-tracker/dto/create-response-tracker.dto.ts b/src/response-tracker/dto/create-response-tracker.dto.ts new file mode 100644 index 0000000..fa33edf --- /dev/null +++ b/src/response-tracker/dto/create-response-tracker.dto.ts @@ -0,0 +1,70 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { ResponseTrackerStatusEnum } from "@prisma/client"; +import { Type } from "class-transformer"; +import { + IsArray, + IsEnum, + IsInt, + IsNotEmpty, + IsNumber, + IsOptional, + IsUUID, + ValidateNested, +} from "class-validator"; +import { AnswerEnum } from "../enums/response-tracker.enums"; + +export class responseObject { + @IsNumber() + @IsNotEmpty() + questionId: number; + + @ApiProperty({ + enum: AnswerEnum, + example: `${AnswerEnum.YES} || ${AnswerEnum.NO} || ${AnswerEnum.DO_NOT_KNOW}`, + }) + @IsEnum(AnswerEnum) + @IsNotEmpty() + answer: AnswerEnum; +} + +export class CreateResponseTrackerDto { + @ApiProperty({ type: "integer", example: 1 }) + @IsNotEmpty() + @IsInt() + surveyFormId: number; + + @ApiProperty({ type: "integer", example: 2 }) + @IsNotEmpty() + @IsUUID() + assesseeId: string; + + @ApiProperty({ type: "integer", example: 3 }) + @IsNotEmpty() + @IsUUID() + assessorId: string; + + @ApiProperty({ + type: responseObject, + isArray: true, + example: [ + { + questionId: 1, + answer: `${AnswerEnum.YES} || ${AnswerEnum.NO} || ${AnswerEnum.DO_NOT_KNOW}`, + }, + ], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => responseObject) + responseJson?: responseObject[] = []; + + @ApiProperty({ + enum: ResponseTrackerStatusEnum, + example: + ResponseTrackerStatusEnum.PENDING || ResponseTrackerStatusEnum.COMPLETED, + }) + @IsNotEmpty() + @IsEnum(ResponseTrackerStatusEnum) + status: ResponseTrackerStatusEnum; +} diff --git a/src/response-tracker/dto/index.ts b/src/response-tracker/dto/index.ts new file mode 100644 index 0000000..0686f6c --- /dev/null +++ b/src/response-tracker/dto/index.ts @@ -0,0 +1,5 @@ +import { from } from "rxjs"; + +export * from "./create-response-tracker.dto"; +export * from "./response-response-tracker.dto"; +export * from "./update-response-tracker.dto"; diff --git a/src/response-tracker/dto/response-response-tracker.dto.ts b/src/response-tracker/dto/response-response-tracker.dto.ts new file mode 100644 index 0000000..096954f --- /dev/null +++ b/src/response-tracker/dto/response-response-tracker.dto.ts @@ -0,0 +1,29 @@ +import { ResponseTrackerStatusEnum } from "@prisma/client"; +import { responseObject } from "./create-response-tracker.dto"; + +export class ResponseTracker { + readonly id: number; + readonly assesseeId: string; + readonly surveyFormId: number; + readonly assessorId: string; + readonly responseJson?: responseObject[]; + readonly status: ResponseTrackerStatusEnum; +} + +export class ResponseTrackerDtoResponse { + data?: ResponseTracker; + message: string; + statusCode?: number; +} + +export class ResponseTrackerDtoMultipleResponse { + data?: ResponseTracker[]; + message: string; + statusCode?: number; +} + +export class SurveyDataResponse{ + totalSurveys: number; + totalSurveysFilled: number; + totalSurveysToBeFilled: number; +} \ No newline at end of file diff --git a/src/response-tracker/dto/update-response-tracker.dto.ts b/src/response-tracker/dto/update-response-tracker.dto.ts new file mode 100644 index 0000000..da7127c --- /dev/null +++ b/src/response-tracker/dto/update-response-tracker.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { + ArrayNotEmpty, + IsArray, + IsInt, + IsNotEmpty, + IsUUID, + ValidateNested, +} from "class-validator"; +import { AnswerEnum } from "../enums/response-tracker.enums"; +import { responseObject } from "./create-response-tracker.dto"; + +export class UpdateResponseTrackerDto { + @ApiProperty({ type: "integer", example: 1 }) + @IsNotEmpty() + @IsInt() + surveyFormId: number; + + @ApiProperty({ type: "integer", example: 2 }) + @IsNotEmpty() + @IsUUID() + assesseeId: string; + + @ApiProperty({ type: "integer", example: 3 }) + @IsNotEmpty() + @IsUUID() + assessorId: string; + + @ApiProperty({ + type: responseObject, + isArray: true, + example: [ + { + questionId: 1, + answer: `${AnswerEnum.YES} || ${AnswerEnum.NO} || ${AnswerEnum.DO_NOT_KNOW}`, + }, + ], + }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => responseObject) + responseJson: responseObject[] = []; +} diff --git a/src/response-tracker/enums/response-tracker.enums.ts b/src/response-tracker/enums/response-tracker.enums.ts new file mode 100644 index 0000000..8ff0435 --- /dev/null +++ b/src/response-tracker/enums/response-tracker.enums.ts @@ -0,0 +1,5 @@ +export enum AnswerEnum { + YES = "Yes", + NO = "No", + DO_NOT_KNOW = "DoNotKnow", +} diff --git a/src/response-tracker/interfaces/response-tracker.interface.ts b/src/response-tracker/interfaces/response-tracker.interface.ts new file mode 100644 index 0000000..f85cb9a --- /dev/null +++ b/src/response-tracker/interfaces/response-tracker.interface.ts @@ -0,0 +1,11 @@ +import { ResponseTrackerStatusEnum } from "@prisma/client"; + +export interface IResponseTracker { + id: number; + status: ResponseTrackerStatusEnum; + Assessor: { + userId: string; + userName: string; + }; +} + diff --git a/src/response-tracker/response-tracker.controller.spec.ts b/src/response-tracker/response-tracker.controller.spec.ts new file mode 100644 index 0000000..cdb9163 --- /dev/null +++ b/src/response-tracker/response-tracker.controller.spec.ts @@ -0,0 +1,216 @@ +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import * as pactum from "pactum"; +import { PrismaService } from "../prisma/prisma.service"; +import { ResponseTrackerModule } from "./response-tracker.module"; +import { CreateResponseTrackerDto } from "./dto"; +import { AnswerEnum } from "./enums/response-tracker.enums"; +import { ResponseTrackerStatusEnum } from "@prisma/client"; + +describe("SurveyFormController e2e", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ResponseTrackerModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + + afterAll(async () => { + await app.close(); + }); + + // beforeEach(async () => { + // // You may want to reset or seed the database before each test. + // // This is not shown in the provided code but is important for reliable testing. + // }); + + describe("Create ResponseTrackerController", () => { + it("should create a new ResponseTracker", async () => { + const createResponseTrackerDto: CreateResponseTrackerDto = { + surveyFormId: 1, + assessorId: "4d62c39b-8102-4c9c-83a9-90b18b02d405", + assesseeId: "a7e57477-9e62-4aa1-8ed6-6d58a74a394f", + responseJson: JSON.parse( + JSON.stringify([ + { + questionId: 1, + answer: `${AnswerEnum.YES}`, + }, + ]) + ), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + await pactum + .spec() + .post("/response-tracker") + .withBody(createResponseTrackerDto) + .expectStatus(201) + .expectJsonMatch({ + message: "Successfully created response tracer", + data: { + ...createResponseTrackerDto, + id: 1, + }, + }); + }); + + it("should return a 400 error if the request body is invalid", async () => { + const invalidCreateResponseTrackerDto = { + surveyFormId: "invalid", + assessorId: "invalid", + assesseeId: "invalid", + response: "invalid", + }; + + await pactum + .spec() + .post("/response-tracker") + .withBody(invalidCreateResponseTrackerDto) + .expectStatus(400); + }); + }); + + describe("GetAllResponseTrackersController", () => { + it("should get all ResponseTrackers", async () => { + const responseResponseTrackerDto = { + id: 1, + surveyFormId: 1, + assessorId: "4d62c39b-8102-4c9c-83a9-90b18b02d405", + assesseeId: "a7e57477-9e62-4aa1-8ed6-6d58a74a394f", + responseJson: JSON.parse( + JSON.stringify([ + { + questionId: 1, + answer: `${AnswerEnum.YES}`, + }, + ]) + ), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + await pactum + .spec() + .get("/response-tracker") + .expectStatus(200) + .expectJsonMatch({ + message: "Successfully fetched all response tracers", + data: [responseResponseTrackerDto], + }); + }); + }); + + describe("GetResponseTrackerByIdController", () => { + it("should get a ResponseTracker by ID", async () => { + const responseResponseTrackerDto = { + id: 1, + surveyFormId: 1, + assessorId: "4d62c39b-8102-4c9c-83a9-90b18b02d405", + assesseeId: "a7e57477-9e62-4aa1-8ed6-6d58a74a394f", + responseJson: JSON.parse( + JSON.stringify([ + { + questionId: 1, + answer: `${AnswerEnum.YES}`, + }, + ]) + ), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + await pactum + .spec() + .get("/response-tracker/1") + .expectStatus(200) + .expectJsonMatch({ + message: "Successfully fetched response tracer for id #1", + data: responseResponseTrackerDto, + }); + }); + + it("should return a 404 error if the ResponseTracker does not exist", async () => { + await pactum.spec().get("/response-tracker/999").expectStatus(404); + }); + }); + + describe("GetResponseTrackersBySurveyFormIdController", () => { + it("should get ResponseTrackers by surveyFormId", async () => { + const responseResponseTrackerDto = { + id: 1, + surveyFormId: 1, + assessorId: "4d62c39b-8102-4c9c-83a9-90b18b02d405", + assesseeId: "a7e57477-9e62-4aa1-8ed6-6d58a74a394f", + responseJson: JSON.parse( + JSON.stringify([ + { + questionId: 1, + answer: `${AnswerEnum.YES}`, + }, + ]) + ), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + await pactum + .spec() + .get("/response-tracker/survey-form/1") + .expectStatus(200) + .expectJsonMatch({ + message: "Successfully fetched response tracer for surveyFormId #1", + data: [responseResponseTrackerDto], + }); + }); + }); + + describe("GetResponseTrackersByAssessorIdController", () => { + it("should get ResponseTrackers by assessorId", async () => { + const responseResponseTrackerDto = { + id: 1, + surveyFormId: 1, + assessorId: "4d62c39b-8102-4c9c-83a9-90b18b02d405", + assesseeId: "a7e57477-9e62-4aa1-8ed6-6d58a74a394f", + responseJson: JSON.parse( + JSON.stringify([ + { + questionId: 1, + answer: `${AnswerEnum.YES}`, + }, + ]) + ), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + await pactum + .spec() + .get("/response-tracker/assessor/4d62c39b-8102-4c9c-83a9-90b18b02d405") + .expectStatus(200) + .expectJsonMatch({ + message: + "Successfully fetched response tracer for assessorId #4d62c39b-8102-4c9c-83a9-90b18b02d405", + data: [responseResponseTrackerDto], + }); + }); + }); +}); diff --git a/src/response-tracker/response-tracker.controller.ts b/src/response-tracker/response-tracker.controller.ts new file mode 100644 index 0000000..5e0621a --- /dev/null +++ b/src/response-tracker/response-tracker.controller.ts @@ -0,0 +1,334 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Logger, + Param, + ParseIntPipe, + ParseUUIDPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { + CreateResponseTrackerDto, + ResponseTrackerDtoMultipleResponse, + ResponseTrackerDtoResponse, + UpdateResponseTrackerDto, +} from "./dto"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { ResponseTrackerService } from "./response-tracker.service"; + +@Controller("response-tracker") +@ApiTags("response-tracker") +export class ResponseTrackerController { + private readonly logger = new Logger(ResponseTrackerController.name); + constructor( + private readonly responseTrackerService: ResponseTrackerService + ) {} + + @Post() + @ApiOperation({ summary: "create new response tracker" }) + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseTrackerDtoResponse }) + async create( + @Res() res, + @Body() createResponseTrackerDto: CreateResponseTrackerDto + ): Promise { + try { + this.logger.log(`Initiated creating response tracker`); + + const response = await this.responseTrackerService.create( + createResponseTrackerDto + ); + + return res.status(HttpStatus.CREATED).json({ + data: response, + message: "Successfully created response tracker", + }); + } catch (error) { + this.logger.error(`Failed to create response tracker`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create response tracker`, + }); + } + } + + @Get() + @ApiOperation({ summary: "get all response trackers" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseTrackerDtoMultipleResponse, + }) + async findAll(@Res() res): Promise { + try { + this.logger.log(`Initiated fetching all response trackers`); + + const responses = await this.responseTrackerService.findAll(); + + return res.status(HttpStatus.OK).json({ + data: responses, + message: `Successfully fetched all response trackers`, + }); + } catch (error) { + this.logger.error(`Failed to fetch all response trackers`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch all response trackers`, + }); + } + } + + @Get(":id") + @ApiOperation({ summary: "get response tracker by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseTrackerDtoResponse }) + async findOne( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated fetching response tracker with id#${id}`); + + const response = await this.responseTrackerService.findOne(id); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched response tracker for id #${id}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch response tracker with id #${id}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to fetch response tracker for id #${id}`, + }); + } + } + + @Get("survey-form/:surveyFormId") + @ApiOperation({ summary: "get response trackers by surveyFormId" }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseTrackerDtoMultipleResponse, + }) + async findBySurveyFormId( + @Res() res, + @Param("surveyFormId", ParseIntPipe) surveyFormId: number + ): Promise { + try { + this.logger.log( + `Initiated fetching response tracker with surveyFormId#${surveyFormId}` + ); + + const response = await this.responseTrackerService.findBySurveyFormId( + surveyFormId + ); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched response tracker for surveyFormId #${surveyFormId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch response tracker with surveyFormId #${surveyFormId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch response tracker for surveyFormId #${surveyFormId}`, + }); + } + } + + @Get("assessor/:assessorId") + @ApiOperation({ + summary: "get response tracker by assessorId", + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseTrackerDtoMultipleResponse, + }) + async findByAssessorId( + @Res() res, + @Param("assessorId", ParseUUIDPipe) assessorId: string + ): Promise { + try { + this.logger.log( + `Initiated fetching response tracker with assessorId#${assessorId}.` + ); + + const response = await this.responseTrackerService.findByAssessorId( + assessorId + ); + + this.logger.log( + `Successfully fetched response tracker for assessorId #${assessorId}.` + ); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched response tracker for assessorId #${assessorId}.`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch response tracker with assessorId #${assessorId}.`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch response tracker for assessorId #${assessorId}.`, + }); + } + } + + @Get("assessee/:assesseeId/:surveyFormId") + @ApiOperation({ + summary: "get response tracker by assesseeId & surveyFormId", + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseTrackerDtoMultipleResponse, + }) + async findByAssesseeId( + @Res() res, + @Param("assesseeId", ParseUUIDPipe) assesseeId: string, + @Param("surveyFormId", ParseIntPipe) surveyFormId: number + ): Promise { + try { + this.logger.log( + `Initiated fetching response tracker with assesseeId#${assesseeId} and surveyFormId #${surveyFormId}` + ); + + const response = + await this.responseTrackerService.findByAssesseeIdAndSurveyFormId( + assesseeId, + surveyFormId + ); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched response tracker for assesseeId #${assesseeId} and surveyFormId ${surveyFormId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch response tracker with assesseeId #${assesseeId} and surveyFormId #${surveyFormId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch response tracker for assesseeId #${assesseeId} and surveyFormId #${surveyFormId}`, + }); + } + } + + @Patch() + @ApiOperation({ + summary: + "update response tracker by surveyFormId, assesseeId and assessorId", + }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseTrackerDtoResponse }) + async update( + @Res() res, + @Body() updateResponseTrackerDto: UpdateResponseTrackerDto + ): Promise { + const { surveyFormId, assesseeId, assessorId } = updateResponseTrackerDto; + try { + this.logger.log( + `Initiated updating response tracker with surveyFormId #${surveyFormId}, assesseeId #${assesseeId} and assessorId #${assessorId}` + ); + + const updatedResponse = + await this.responseTrackerService.updateBySurveyFormId( + updateResponseTrackerDto + ); + + return res.status(HttpStatus.OK).json({ + data: updatedResponse, + message: `Successfully updated response tracker with surveyFormId ${surveyFormId}, assesseeId #${assesseeId} and assessorId #${assessorId}`, + }); + } catch (error) { + this.logger.error( + `Failed to update response tracker with surveyFormId #${surveyFormId}, assesseeId #${assesseeId} and assessorId #${assessorId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to update response tracker with surveyFormId #${surveyFormId}, assesseeId #${assesseeId} and assessorId #${assessorId}`, + }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "delete response tracker by id" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseTrackerDtoResponse }) + async remove( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated deleting response tracker with id #${id}`); + + await this.responseTrackerService.remove(id); + + return res.status(HttpStatus.OK).json({ + message: `Successfully deleted response tracker with id ${id}`, + }); + } catch (error) { + this.logger.error( + `Failed to delete response tracker with id #${id}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to delete response tracker with id #${id}`, + }); + } + } +} diff --git a/src/response-tracker/response-tracker.module.ts b/src/response-tracker/response-tracker.module.ts new file mode 100644 index 0000000..54eca4b --- /dev/null +++ b/src/response-tracker/response-tracker.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { ResponseTrackerService } from "./response-tracker.service"; +import { ResponseTrackerController } from "./response-tracker.controller"; +import { PrismaService } from "..//prisma/prisma.service"; + +@Module({ + controllers: [ResponseTrackerController], + providers: [ResponseTrackerService, PrismaService], + exports: [ResponseTrackerService], +}) +export class ResponseTrackerModule {} diff --git a/src/response-tracker/response-tracker.service.spec.ts b/src/response-tracker/response-tracker.service.spec.ts new file mode 100644 index 0000000..bc48094 --- /dev/null +++ b/src/response-tracker/response-tracker.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ResponseTrackerService } from './response-tracker.service'; + +describe('ResponseTrackerService', () => { + let service: ResponseTrackerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ResponseTrackerService], + }).compile(); + + service = module.get(ResponseTrackerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/response-tracker/response-tracker.service.ts b/src/response-tracker/response-tracker.service.ts new file mode 100644 index 0000000..3b68cbb --- /dev/null +++ b/src/response-tracker/response-tracker.service.ts @@ -0,0 +1,345 @@ +import { PrismaService } from "../prisma/prisma.service"; +import { + Injectable, + NotAcceptableException, + NotFoundException, +} from "@nestjs/common"; +import _ from "lodash"; +import { + CreateResponseTrackerDto, + responseObject, +} from "./dto/create-response-tracker.dto"; +import { UpdateResponseTrackerDto } from "./dto/update-response-tracker.dto"; +import { IResponseTracker } from "./interfaces/response-tracker.interface"; +import { ResponseTrackerStatusEnum } from "@prisma/client"; +import { SurveyDataResponse } from "./dto"; + +@Injectable() +export class ResponseTrackerService { + constructor(private prisma: PrismaService) {} + + public async create(createResponseTrackerDto: CreateResponseTrackerDto) { + const responseJson = JSON.stringify(createResponseTrackerDto.responseJson); + const payload = { + ...createResponseTrackerDto, + responseJson: JSON.parse(responseJson), + }; + + return await this.prisma.responseTracker.create({ + data: payload, + }); + } + + public async findAll() { + return await this.prisma.responseTracker.findMany(); + } + + public async findOne(id: number) { + const response = await this.prisma.responseTracker.findUnique({ + where: { id }, + }); + if (!response) + throw new NotFoundException(`Response tracker with id #${id} not found`); + return response; + } + + public async findBySurveyFormId(surveyFormId: number) { + const response = await this.prisma.responseTracker.findMany({ + where: { surveyFormId }, + }); + + if (!response || response.length === 0) + throw new NotFoundException( + `Response tracker with surveyFormId #${surveyFormId} not found` + ); + return response; + } + + public async findByAssessorId( + assessorId: string + ) { + const user = await this.prisma.userMetadata.findUnique({ + where: { userId: assessorId }, + }); + + if (!user) { + throw new NotFoundException( + `Assessor with user id #${assessorId} not found.` + ); + } + + const response = await this.prisma.responseTracker.findMany({ + where: { + assessorId, + surveyForm: { + SurveyConfig: { + isActive: true, + }, + }, + }, + include: { + Assessee: { + select: { + designation: true, + userName: true, + profilePicture: true, + }, + }, + surveyForm: { + select: { + SurveyConfig: { + select: { + endTime: true, + }, + }, + }, + }, + }, + }); + + return response; + } + + public async findByAssesseeIdAndSurveyFormId( + assesseeId: string, + surveyFormId: number + ) { + const response = await this.prisma.responseTracker.findMany({ + where: { assesseeId, surveyFormId }, + include: { + Assessor: { + select: { + designation: true, + userName: true, + profilePicture: true, + }, + }, + surveyForm: { + select: { + SurveyConfig: { + select: { + endTime: true, + }, + }, + }, + }, + }, + }); + + if (!response || response.length === 0) + throw new NotFoundException( + `Response tracker with assesseeId #${assesseeId} and surveyFormId #${surveyFormId} not found` + ); + return response; + } + + public async getResponsesActiveSurveyByUserId(userId: string) { + const surveyForms = await this.prisma.surveyForm.findMany({ + where: { + SurveyConfig: { + isActive: true, + }, + userId, + ResponseTracker: { + every: { + assesseeId: userId, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + select: { + ResponseTracker: { + select: { + id: true, + status: true, + Assessor: { + select: { + userId: true, + userName: true, + }, + }, + }, + }, + }, + }); + const responses: IResponseTracker[] = surveyForms.flatMap( + (form) => form.ResponseTracker + ); + return responses; + } + + public async update( + id: number, + updateResponseTrackerDto: UpdateResponseTrackerDto + ) { + const isQuestionsValid = await this.validateQuestions( + updateResponseTrackerDto.responseJson, + updateResponseTrackerDto.surveyFormId + ); + + if (!isQuestionsValid) { + throw new NotAcceptableException( + `Question not matching with the questions in the survey form with id #${updateResponseTrackerDto.surveyFormId}` + ); + } + const responseJson = JSON.stringify(updateResponseTrackerDto.responseJson); + + const payload = { + ...updateResponseTrackerDto, + responseJson: JSON.parse(responseJson || "[]"), + }; + + if (!responseJson) delete payload.responseJson; + + return await this.prisma.responseTracker.update({ + where: { id }, + data: payload, + }); + } + + public async remove(id: number) { + return await this.prisma.responseTracker.delete({ where: { id } }); + } + + async getAllResponseJsonBySurveyFormId( + surveyFormId: number, + status?: string + ) { + const whereClause: any = { + surveyFormId: surveyFormId, + }; + + if (status) { + whereClause.status = status; // Filter by status if provided + } + + const responseTrackers = await this.prisma.responseTracker.findMany({ + where: whereClause, + select: { + responseJson: true, + status: true, + assessorId: true, + assesseeId: true, + }, + }); + + const result = responseTrackers.map((response) => { + const { assesseeId, assessorId, responseJson, status } = response; + return { + assesseeId, + assessorId, + status, + responseJson: JSON.parse(JSON.stringify(responseJson) || "[]"), + }; + }); + + return result; + } + + public async updateBySurveyFormId( + updateResponseTrackerDto: UpdateResponseTrackerDto + ) { + const { surveyFormId, assesseeId, assessorId, responseJson } = + updateResponseTrackerDto; + const isQuestionsValid = await this.validateQuestions( + responseJson, + surveyFormId + ); + + if (!isQuestionsValid) { + throw new NotAcceptableException( + `Question not matching with the questions in the survey form with id #${surveyFormId}` + ); + } + + const responseJsonData = JSON.stringify(responseJson); + + const payload = { + responseJson: JSON.parse(responseJsonData || "[]"), + status: ResponseTrackerStatusEnum.COMPLETED, + }; + + if (!responseJsonData) delete payload.responseJson; + + const today = new Date().toISOString(); + + return await this.prisma.responseTracker.update({ + where: { + surveyFormId_assesseeId_assessorId: { + surveyFormId, + assesseeId, + assessorId, + }, + surveyForm: { + SurveyConfig: { + endTime: { + gte: today, + }, + }, + }, + }, + data: payload, + }); + } + + public async validateQuestions( + questionData: responseObject[], + surveyFormId: number + ): Promise { + const surveyForm = await this.prisma.surveyForm.findUniqueOrThrow({ + where: { + id: surveyFormId, + }, + }); + + const surveyFormQuestions = JSON.parse( + JSON.stringify(_.get(surveyForm, "questionsJson", [])) + ); + let surveyFormQuestionIds = _.compact( + _.map(surveyFormQuestions, (question) => + _.get(question, "questionId", null) + ) + ); + + surveyFormQuestionIds = _.sortBy(surveyFormQuestionIds); + + let questionIds = _.compact( + _.map(questionData, (question) => _.get(question, "questionId", null)) + ); + + questionIds = _.sortBy(questionIds); + return _.isEqual(questionIds, surveyFormQuestionIds); + } + + async fetchActiveSurveyFormData(): Promise { + const isActiveSurveyCondition = { + surveyForm: { + SurveyConfig: { + isActive: true, + }, + }, + }; + + const totalSurveys = await this.getCountByStatus(isActiveSurveyCondition); + + const totalSurveysToBeFilled = await this.getCountByStatus({ + ...isActiveSurveyCondition, + status: ResponseTrackerStatusEnum.PENDING, + }); + + const totalSurveysFilled = await this.getCountByStatus({ + ...isActiveSurveyCondition, + status: ResponseTrackerStatusEnum.COMPLETED, + }); + + return { totalSurveys, totalSurveysFilled, totalSurveysToBeFilled }; + } + + async getCountByStatus(whereCondition): Promise { + return await this.prisma.responseTracker.count({ + where: whereCondition, + }); + } +} diff --git a/src/scheduled-tasks/scheduled-tasks.module.ts b/src/scheduled-tasks/scheduled-tasks.module.ts new file mode 100644 index 0000000..fb6eb4a --- /dev/null +++ b/src/scheduled-tasks/scheduled-tasks.module.ts @@ -0,0 +1,35 @@ +import { Module } from "@nestjs/common"; +import { QuestionBankModule } from "../question-bank/question-bank.module"; +import { ResponseTrackerService } from "../response-tracker/response-tracker.service"; +import { SurveyConfigModule } from "../survey-config/survey-config.module"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import { SurveyScoreService } from "../survey-score/survey-score.service"; +import { SurveyService } from "../survey/survey.service"; +import { UserMetadataModule } from "../user-metadata/user-metadata.module"; +import { ScheduledTasksService } from "./scheduled-tasks.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { AdminCompetencyModule } from "src/admin-competency/admin-competency.module"; +import { SunbirdRcModule } from "src/external-services/sunbird-rc/sunbird-rc.module"; +import { PassbookModule } from "src/external-services/passbook/passbook.module"; +import { TarentoModule } from "src/external-services/tarento/tarento.module"; + +@Module({ + imports: [ + QuestionBankModule, + UserMetadataModule, + SurveyConfigModule, + AdminCompetencyModule, + SunbirdRcModule, + PassbookModule, + TarentoModule + ], + providers: [ + ScheduledTasksService, + SurveyFormService, + SurveyService, + SurveyScoreService, + ResponseTrackerService, + PrismaService, + ], +}) +export class ScheduledTasksModule {} diff --git a/src/scheduled-tasks/scheduled-tasks.service.ts b/src/scheduled-tasks/scheduled-tasks.service.ts new file mode 100644 index 0000000..be81bba --- /dev/null +++ b/src/scheduled-tasks/scheduled-tasks.service.ts @@ -0,0 +1,172 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron, CronExpression } from "@nestjs/schedule"; +import { SurveyConfig, SurveyStatusEnum } from "@prisma/client"; +import { PrismaService } from "../prisma/prisma.service"; +import { SurveyConfigService } from "../survey-config/survey-config.service"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import { SurveyScoreService } from "../survey-score/survey-score.service"; +import { SurveyService } from "../survey/survey.service"; +import { UserMetadataService } from "../user-metadata/user-metadata.service"; +import { isDayBeforeToday, isToday, isTomorrow } from "../utils/utils"; +import { PassbookService } from "src/external-services/passbook/passbook.service"; +import { SunbirdRcService } from "src/external-services/sunbird-rc/sunbird-rc.service"; +import { TarentoService } from "src/external-services/tarento/tarento.service"; +import { AdminCompetencyService } from "src/admin-competency/admin-competency.service"; + +@Injectable() +export class ScheduledTasksService { + constructor( + private prismaService: PrismaService, + private surveyConfigService: SurveyConfigService, + private surveyService: SurveyService, + private userMetadataService: UserMetadataService, + private surveyFormService: SurveyFormService, + private surveyScoreService: SurveyScoreService, + private passbookService: PassbookService, + private sunbirdRcService: SunbirdRcService, + private tarentoService: TarentoService, + private adminCompetency: AdminCompetencyService + ) {} + private readonly logger = new Logger(ScheduledTasksService.name); + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async surveyCron() { + this.logger.log(`Start CRON Job for Surveys. Fetching data of all survey configurations.`); + + const surveyConfigs = await this.surveyConfigService.getAllSurveyConfig({}); + + this.logger.log(`Fetched data of all survey configurations. Cycling through every survey configuration to start or end surveys.`); + + for (const surveyConfig of surveyConfigs) { + this.logger.log(`Checking if the surveyConfig "${surveyConfig.surveyName}" with id: "${surveyConfig.id}" is Active.`); + + if (surveyConfig.isActive == true) { + this.logger.log(`The survey #${surveyConfig.surveyName} is ACTIVE.`); + + if (isDayBeforeToday(surveyConfig.endTime)) { + this.logger.log(`Survey will end tomorrow. Sending remainder notification to all survey participants.`); + //send notifications + } else if (isToday(surveyConfig.endTime)) { + this.logger.log(`Fetching all SurveyForms related to the survey.`); + const surveyForms = + await this.surveyFormService.findSurveyFormBySurveyConfigId( + surveyConfig.id + ); + + this.logger.log(`Calculation scores for all "${surveyForms.length}" SurveyForms related to the survey.`); + for (const surveyForm of surveyForms) { + + this.logger.log(`Updating the surveyFrom's (id: "${surveyForm.id}") status to "CLOSED".`); + await this.surveyFormService.updateSurveyFormStatus( + surveyForm.id, + SurveyStatusEnum.CLOSED + ); + + this.logger.log(`Calculating the score for SurveyForm with id: "${surveyForm.id}" for user with id: "${surveyForm.userId}".`); + await this.surveyScoreService.calculateSurveyScoreBySurveyFormId( + surveyForm.id + ); + this.logger.log(`Calculated the score for SurveyForm with id: "${surveyForm.id}" for user with id: "${surveyForm.userId}".`); + + + this.logger.log(`Fetching the data to create a sunbirdRC certificate.`); + const credData = await this.surveyService.createCredentialSchemaBySurveyFormId(surveyForm.id); + + this.logger.log(`Issuing sunbirdRC credentials.`); + let sunbirdCred : any; + try { + sunbirdCred = await this.sunbirdRcService.issueCredential(credData); + } catch (error) { + this.logger.error(error); + } + + if(sunbirdCred){ + try { + this.logger.log(`Credentials issued. Pushing credentials to passbook.`); + await this.passbookService.addFeedback({ + ...credData, + certificateId: sunbirdCred["credentail"]["id"] + }); + this.logger.log(`Added credentials to passbook."`); + + } catch (error) { + this.logger.error(error); + } + + this.logger.log(`Updating credentials and overall score in the surveyFrom.`); + await this.surveyFormService.updateOverallScoreAndCredentialDid(surveyForm.id, credData.overallScore, sunbirdCred["credentail"]["id"]); + + } + } + + this.logger.log(`Deleting the userMapping for the survey as it will end today.`); + await this.prismaService.userMapping.deleteMany({ + where: { surveyConfigId: surveyConfig.id }, + }); + + this.logger.log(`Deactivating the Survey "${surveyConfig.surveyName}" as it will end today.`); + await this.surveyConfigService.deactivateSurveyConfig( + surveyConfig.id + ); + this.logger.log(`The Survey "${surveyConfig.surveyName}" ends today.`); + + } else { + this.logger.log(`The survey #"${surveyConfig.surveyName}" with surveyConfigId: "${surveyConfig.id}" is ongoing.`); + } + } else { + await this.startSurvey(surveyConfig); + } + } + } + + async startSurvey(surveyConfig: SurveyConfig) { + this.logger.log(`The survey with surveyConfigId: "${surveyConfig.id}" is not ACTIVE.`); + if (isTomorrow(surveyConfig.startTime)) { + //update user data + this.logger.log(`Fetching userIds of the users who are participating in the survey.`); + const updateUsers = await this.surveyService.fetchUserIdsOfSurveyParticipants( + surveyConfig.id + ); + this.logger.log(`Updating all "${updateUsers.size}" user's data who are participating in the survey.`); + for (const userId of updateUsers) { + await this.userMetadataService.createOrUpdateUserMetadata(userId); + } + + } else if (isToday(surveyConfig.startTime)) { + + this.logger.log(`Activating the Survey "${surveyConfig.surveyName}" with surveyConfigId: "${surveyConfig.id}".`); + await this.surveyConfigService.updateSurveyConfigById(surveyConfig.id, { + isActive: true, + }); + + this.logger.log(`Generating SurveyForms for every user who is participating in the Survey "${surveyConfig.surveyName}" with id: "${surveyConfig.id}".`); + await this.surveyService.generateSurveyFormsForSurveyConfig( + surveyConfig.id + ); + this.logger.log(`The Survey "${surveyConfig.surveyName}" with id: "${surveyConfig.id}" has been started.`); + + // send notifications for all survey participant + // this.logger.log( + // `Sending notification to every user who is participating in the Survey "${surveyConfig.surveyName}" with id: "${surveyConfig.id}".` + // ); + } else { + this.logger.log(`The survey with surveyConfigId: "${surveyConfig.id}" is to be Activated in the future or a dead survey.`); + } + } + + + @Cron(process.env.FRAC_CRON_EPX ?? CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT) + async fracSyncCron(){ + this.logger.log(`Initialized Syncing FRAC data`); + await this.tarentoService.formatAndSyncFracData(); + await this.adminCompetency.syncCompetencyData(); + this.logger.log(`Successfully synced FRAC data`); + } + + @Cron(process.env.USER_CRON_EPX ?? CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT) + async userSyncCron(){ + this.logger.log(`Initialized Syncing User data`); + await this.userMetadataService.syncUserDataWithFrac(); + this.logger.log(`Successfully synced User data`); + } +} diff --git a/src/survey-config/dto/create-survey-config.dto.ts b/src/survey-config/dto/create-survey-config.dto.ts new file mode 100644 index 0000000..6be0694 --- /dev/null +++ b/src/survey-config/dto/create-survey-config.dto.ts @@ -0,0 +1,99 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { TimeUnitsEnum } from "@prisma/client"; +import { Type } from "class-transformer"; +import { + ArrayNotEmpty, + IsArray, + IsDate, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from "class-validator"; +import { UUID } from "crypto"; +import { IsTwoFieldsRequired } from "../../utils/custom-decorators/isTwoFieldsRequired"; + +export class CreateSurveyConfigDto { + + @IsNotEmpty() + @IsString() + surveyName: string; + + @IsOptional() + @IsNotEmpty() + @IsTwoFieldsRequired("onboardingTime", "onboardingTimeUnit") + @IsInt() + onboardingTime?: number; + + @IsOptional() + @IsNotEmpty() + @ApiProperty({ enum: TimeUnitsEnum }) + @IsTwoFieldsRequired("onboardingTimeUnit", "onboardingTime") + @IsEnum(TimeUnitsEnum, { each: true }) + onboardingTimeUnit?: TimeUnitsEnum; + + // start time for the survey config + @IsNotEmpty() + @IsDate() + @Type(() => Date) + @IsTwoFieldsRequired("startTime", "endTime") + startTime: Date; + + // end time for the survey config + @IsNotEmpty() + @IsDate() + @Type(() => Date) + @IsTwoFieldsRequired("endTime", "startTime") + endTime: Date; + + @ApiProperty({ + type: "string" || "number", + format: "binary", + }) + file: any; +} + +// SurveyConfigFilterDto is used for filtering and paginating survey config. +export class SurveyConfigFilterDto { + // Optional maxQuestions filter validate that its a number + @IsOptional() + @IsNotEmpty() + @IsInt() + configId?: number; + + //Optional startTime filter validate that its valid date-time value + @IsOptional() + @IsNotEmpty() + @IsDate() + startTime?: Date; + + //Optional endTime filter validate that its valid date-time value. + @IsOptional() + @IsNotEmpty() + @IsDate() + endTime?: Date; + + // Optional limit for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + limit?: number; + + // Optional offset for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + offset?: number; +} + +export class UserMappingFileUploadDto { + @IsNotEmpty() + @IsUUID() + assesseeId: string; + + @IsNotEmpty() + @IsArray() + @ArrayNotEmpty() + @IsUUID("all", { each: true }) + assessorIds: UUID[]; +} diff --git a/src/survey-config/dto/response-survey-config.dto.ts b/src/survey-config/dto/response-survey-config.dto.ts new file mode 100644 index 0000000..363360b --- /dev/null +++ b/src/survey-config/dto/response-survey-config.dto.ts @@ -0,0 +1,26 @@ +import { TimeUnitsEnum } from "@prisma/client"; + +export class ResponseSurveyConfigDto { + readonly id: number; + readonly surveyName: string; + readonly onboardingTime: number; + readonly onboardingTimeUnit: TimeUnitsEnum; + readonly startTime: Date; + readonly endTime: Date; + readonly isActive: boolean; +} + +export class ConfigResponseDTO { + readonly data: ResponseSurveyConfigDto; + readonly message: string; +} + +export class UserMappingDTO { + assesseeId: string; + assessorIds: string; +} + +export class SampleUserMappingResponse { + data: UserMappingDTO[]; + message: string; +} diff --git a/src/survey-config/dto/update-survey-config.dto.ts b/src/survey-config/dto/update-survey-config.dto.ts new file mode 100644 index 0000000..9263ef7 --- /dev/null +++ b/src/survey-config/dto/update-survey-config.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateSurveyConfigDto } from './create-survey-config.dto'; +import { IsBoolean, IsDate, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsTwoFieldsRequired } from '../../utils/custom-decorators/isTwoFieldsRequired'; +import { TimeUnitsEnum } from '@prisma/client'; +import { Type } from 'class-transformer'; + +export class UpdateSurveyConfigDto { + + @IsOptional() + @IsNotEmpty() + @IsString() + surveyName?: string; + + @IsOptional() + @IsNotEmpty() + @IsTwoFieldsRequired("onboardingTime", "onboardingTimeUnit") + @IsInt() + onboardingTime?: number; + + @IsOptional() + @IsNotEmpty() + @ApiProperty({ enum: TimeUnitsEnum }) + @IsTwoFieldsRequired("onboardingTimeUnit", "onboardingTime") + @IsEnum(TimeUnitsEnum, { each: true }) + onboardingTimeUnit?: TimeUnitsEnum; + + @IsOptional() + @IsNotEmpty() + @IsDate() + @Type(() => Date) + startTime?: Date; + + @IsOptional() + @IsNotEmpty() + @IsDate() + @Type(() => Date) + endTime?: Date; + + @IsOptional() + @ApiProperty({ + type: "string" || "number", + format: "binary", + }) + file?: any; + + @IsOptional() + @IsNotEmpty() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/survey-config/survey-config.controller.spec.ts b/src/survey-config/survey-config.controller.spec.ts new file mode 100644 index 0000000..d85dd07 --- /dev/null +++ b/src/survey-config/survey-config.controller.spec.ts @@ -0,0 +1,150 @@ +import * as pactum from "pactum"; +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { Test, TestingModule } from "@nestjs/testing"; +import { SurveyConfigModule } from "./survey-config.module"; +import { ConfigService } from "@nestjs/config"; +import { CreateSurveyConfigDto } from "./dto/create-survey-config.dto"; + +describe("SurveyConfig e2e", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [SurveyConfigModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("SurveyConfigController createSurveyConfig()", () => { + it("should create a new survey config", async () => { + const createSurveyConfigDto : CreateSurveyConfigDto = { + startTime: new Date("2023-10-31"), + endTime: new Date("2024-01-31"), + departmentId: 1, + onboardingTime: 30, + onboardingTimeUnit: "DAY", + }; + + const response = await pactum + .spec() + .post("/survey-config") + .withBody(createSurveyConfigDto) + .expectStatus(201); + + const createdSurveyConfig = JSON.parse(JSON.stringify(response.body)); + expect(createdSurveyConfig.message).toEqual( + `Survey config created successfully` + ); + expect(createdSurveyConfig.data).toHaveProperty("id") + expect(createdSurveyConfig.data.departmentId).toEqual( + createSurveyConfigDto.departmentId + ); + expect(createdSurveyConfig.data.startTime).toBeDefined() + expect(createdSurveyConfig.data.endTime).toBeDefined() + expect(createdSurveyConfig.data.onboardingTime).toEqual(createSurveyConfigDto.onboardingTime); + expect(createdSurveyConfig.data.onboardingTimeUnit).toEqual(createSurveyConfigDto.onboardingTimeUnit); + }); + }); + + describe("SurveyConfigController getAllSurveyConfig()", () => { + it("should get all survey configs", async () => { + const response = await pactum + .spec() + .get("/survey-config") + .expectStatus(200); + + const surveyConfigs = JSON.parse(JSON.stringify(response.body)); + expect(surveyConfigs.message).toEqual( + "Successfully fetched all survey config." + ); + expect(surveyConfigs.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("SurveyConfigController updateSurveyConfigById()", () => { + it("should update an existing survey config", async () => { + const testData = { + id: 1, + }; + const updateSurveyConfigDto = { + onboardingTime: 2, + onboardingTimeUnit: "MONTH", + }; + + const response = await pactum + .spec() + .patch(`/survey-config/update/${testData.id}`) + .withPathParams({ + id: testData.id, + }) + .withBody(updateSurveyConfigDto) + .expectStatus(200); + + const updatedSurveyConfig = JSON.parse(JSON.stringify(response.body)); + + expect(updatedSurveyConfig.message).toEqual( + `Successfully updated survey config for id #${testData.id}` + ); + expect(updatedSurveyConfig.data.id).toEqual(testData.id); + expect(updatedSurveyConfig.data.departmentId).toBeDefined(); + expect(updatedSurveyConfig.data.onboardingTime).toEqual( + updateSurveyConfigDto.onboardingTime + ); + expect(updatedSurveyConfig.data.onboardingTimeUnit).toEqual( + updateSurveyConfigDto.onboardingTimeUnit + ); + expect(updatedSurveyConfig.data.startTime).toBeDefined(); + expect(updatedSurveyConfig.data.endTime).toBeDefined(); + }); + }); + + describe("SurveyConfigController deleteSurveyConfigById()", () => { + it("should delete an existing survey config", async () => { + const testData = { + id: 1, + }; + const response = await pactum + .spec() + .delete(`/survey-config/delete/${testData.id}`) + .withPathParams({ + id: testData.id, + }) + .expectStatus(200); + + const deletedSurveyConfig = JSON.parse(JSON.stringify(response.body)); + expect(deletedSurveyConfig.message).toEqual( + `Successfully deleted survey config for id #${testData.id}` + ); + expect(deletedSurveyConfig.data.id).toEqual(testData.id); + expect(deletedSurveyConfig.data.departmentId).toBeDefined(); + expect(deletedSurveyConfig.data.onboardingTime).toBeDefined(); + expect(deletedSurveyConfig.data.onboardingTimeUnit).toBeDefined(); + expect(deletedSurveyConfig.data.startTime).toBeDefined(); + expect(deletedSurveyConfig.data.endTime).toBeDefined(); + }); + }); +}); diff --git a/src/survey-config/survey-config.controller.ts b/src/survey-config/survey-config.controller.ts new file mode 100644 index 0000000..36e337f --- /dev/null +++ b/src/survey-config/survey-config.controller.ts @@ -0,0 +1,219 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Delete, + Logger, + HttpStatus, + Res, + Query, + Param, + ParseIntPipe, + UseInterceptors, + UploadedFile, +} from "@nestjs/common"; +import { SurveyConfigService } from "./survey-config.service"; +import { + CreateSurveyConfigDto, + SurveyConfigFilterDto, +} from "./dto/create-survey-config.dto"; +import { UpdateSurveyConfigDto } from "./dto/update-survey-config.dto"; +import { ApiBody, ApiConsumes, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { ConfigResponseDTO, ResponseSurveyConfigDto, SampleUserMappingResponse } from "./dto/response-survey-config.dto"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { multerOptions } from "../config/multer-options.config"; + +@Controller("survey-config") +@ApiTags("survey-config") +export class SurveyConfigController { + // Create a logger instance for this controller to log events and errors. + private readonly logger = new Logger(SurveyConfigController.name); + + // The survey config controller class, injecting the survey config service. + constructor(private readonly surveyConfigService: SurveyConfigService) {} + + @Post() + @ApiConsumes("multipart/form-data") + @ApiOperation({ summary: "Create a survey config" }) // Describes the api operation for Swagger. + @UseInterceptors(FileInterceptor("file", multerOptions)) + @ApiResponse({ status: HttpStatus.CREATED, type: ConfigResponseDTO }) + async createSurveyConfig( + @Res() res, + @UploadedFile() file, + @Body() createSurveyConfigDto: CreateSurveyConfigDto + ): Promise { + try { + // Log the initiation for the Survey config creation + this.logger.log(`Creating a new survey config`); + const createdSurveyConfig = + await this.surveyConfigService.createSurveyConfig( + file.path, createSurveyConfigDto + ); + + // Log the successful creation of the survey config + this.logger.log(`Successfully created the new survey config.`); + + // Return a success response with the created survey config data + return res.status(HttpStatus.CREATED).json({ + message: "Survey config created successfully", + data: createdSurveyConfig, + }); + } catch (error) { + this.logger.error(`Failed to create new survey config.`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + // Return an error response + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create new survey config.`, + }); + } + } + + // Get all survey config + @Get() + @ApiOperation({ summary: "Get all Survey config" }) // Api operation for swagger + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseSurveyConfigDto, + isArray: true, + }) // Api response for Swagger. + async getAllSurveyConfig( + @Res() res, + @Query() filter: SurveyConfigFilterDto + ): Promise { + try { + this.logger.log(`Initiated fetching all the survey config.`); + const surveyConfigs = await this.surveyConfigService.getAllSurveyConfig( + filter + ); + this.logger.log(`Successfully fetched survey config`); + + return res.status(HttpStatus.OK).json({ + message: "Successfully fetched all survey config.", + data: surveyConfigs, + }); + } catch (error) { + this.logger.error(`Failed to fetch survey configs`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to Get all survey config.`, + }); + } + } + + // Update the survey config + @Patch("/update/:id") + @ApiConsumes("multipart/form-data") + @UseInterceptors(FileInterceptor("file", multerOptions)) + @ApiOperation({ summary: "Update the survey config by ID" }) // Api operation for swagger + @ApiResponse({ status: HttpStatus.OK, type: ConfigResponseDTO }) // Api operation for swagger + async updateSurveyConfigById( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @UploadedFile() file, + @Body() updateSurveyConfigDto: UpdateSurveyConfigDto + ): Promise { + try { + this.logger.log(`Initiated updating the survey config for id #${id}`); + const updateSurveyConfig = + await this.surveyConfigService.updateSurveyConfigById( + id, + updateSurveyConfigDto, + file ? file.path: null, + ); + this.logger.log(`Successfully updated survey config for id #${id}`); + return res.status(HttpStatus.OK).json({ + message: `Successfully updated survey config for id #${id}`, + data: updateSurveyConfig, + }); + } catch (error) { + this.logger.error(`Failed to update survey config for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to update survey config for id #${id}`, + }); + } + } + + // Delete the survey config + @Delete("/delete/:id") + @ApiOperation({ summary: "Delete the survey config by Id" }) // Api operation for swagger + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseSurveyConfigDto, + }) + async deleteSurveyConfigById( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log( + `Initiating deleting of a survey config with an id ${id}` + ); + + const deletedSurveyConfig = + await this.surveyConfigService.deleteSurveyConfig(id); + + return res.status(HttpStatus.OK).json({ + message: `Successfully deleted survey config for id #${id}`, + data: deletedSurveyConfig, + }); + } catch (error) { + this.logger.error(`Failed to delete survey config for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to delete survey config for id #${id}`, + }); + } + } + + @Get("user-mapping-sample") + @ApiOperation({ summary: "Get sample user mapping data" }) // Api operation for swagger + @ApiResponse({ + status: HttpStatus.OK, + type: SampleUserMappingResponse, + isArray: true, + }) + async getUserMappingSampleForSurveyConfig( + @Res() res + ): Promise { + try { + this.logger.log(`Initiated fetching sample user mapping data.`); + const sampleData = await this.surveyConfigService.getUserMappingSampleForSurveyConfig(); + this.logger.log(`Successfully fetched sample user mapping data.`); + + return res.status(HttpStatus.OK).json({ + message: "Kindly substitute the placeholder UUIDs with authentic and valid ones.", + data: sampleData, + }); + } catch (error) { + this.logger.error(`Failed to fetch sample user mapping data`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); // get error message and status code + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch sample user mapping data.`, + }); + } + } +} diff --git a/src/survey-config/survey-config.module.ts b/src/survey-config/survey-config.module.ts new file mode 100644 index 0000000..743f04d --- /dev/null +++ b/src/survey-config/survey-config.module.ts @@ -0,0 +1,16 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { FileUploadService } from "../file-upload/file-upload.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { UserMetadataModule } from "../user-metadata/user-metadata.module"; +import { SurveyConfigController } from "./survey-config.controller"; +import { SurveyConfigService } from "./survey-config.service"; +import { CredentialDIDService } from "src/credential-did/credential-did.service"; +import { SunbirdRcService } from "src/external-services/sunbird-rc/sunbird-rc.service"; + +@Module({ + imports: [forwardRef(() => UserMetadataModule)], + controllers: [SurveyConfigController], + providers: [SurveyConfigService, FileUploadService, PrismaService,CredentialDIDService, SunbirdRcService], + exports: [SurveyConfigService], +}) +export class SurveyConfigModule {} diff --git a/src/survey-config/survey-config.service.ts b/src/survey-config/survey-config.service.ts new file mode 100644 index 0000000..8f18ed2 --- /dev/null +++ b/src/survey-config/survey-config.service.ts @@ -0,0 +1,221 @@ +import { + Inject, + Injectable, + NotAcceptableException, + NotFoundException, + forwardRef, +} from "@nestjs/common"; +import { FileUploadService } from "../file-upload/file-upload.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { + CreateSurveyConfigDto, + SurveyConfigFilterDto, + UserMappingFileUploadDto, +} from "./dto/create-survey-config.dto"; +import { UpdateSurveyConfigDto } from "./dto/update-survey-config.dto"; +import { UserMetadataService } from "../user-metadata/user-metadata.service"; +import { isDateInPast } from "../utils/utils"; +import { UserMappingDTO } from "./dto/response-survey-config.dto"; +import { CredentialDIDService } from "src/credential-did/credential-did.service"; + +@Injectable() +export class SurveyConfigService { + constructor( + private prisma: PrismaService, + private fileUploadService: FileUploadService, + @Inject(forwardRef(() => UserMetadataService)) + private userMetadataService: UserMetadataService, + private credentialService: CredentialDIDService + ) {} + async createSurveyConfig( + filepath, + createSurveyConfigDto: CreateSurveyConfigDto + ) { + let newConfig; + let userIdList: Set = new Set(); + try { + // await this.credentialService.findDIDs(); + await this.prisma.$transaction(async (prismaClient) => { + newConfig = await prismaClient.surveyConfig.create({ + data: { + surveyName: createSurveyConfigDto.surveyName, + endTime: createSurveyConfigDto.endTime, + startTime: createSurveyConfigDto.startTime, + }, + }); + + const parsedData = await this.fileUploadService.parseCSV(filepath); + + for (const obj of parsedData) { + obj.assessorIds = obj.assessorIds.replace(/\s/g, '').split(","); + + const userMapping: UserMappingFileUploadDto = obj; + + try { + userIdList = await this.userMetadataService.validateAndFetchUserIds( + prismaClient, + userIdList, + userMapping + ); + } catch (error) { + throw error; + } + + await prismaClient.userMapping.create({ + data: { + surveyConfigId: newConfig.id, + assesseeId: userMapping.assesseeId, + assessorIds: userMapping.assessorIds, + }, + }); + } + }); + await this.fileUploadService.deleteUploadedFile(filepath); + } catch (error) { + await this.fileUploadService.deleteUploadedFile(filepath); + throw error; + } + + return newConfig; + } + + async getAllSurveyConfig(filter: SurveyConfigFilterDto) { + const { configId, startTime, endTime, limit, offset } = filter; + return await this.prisma.surveyConfig.findMany({ + where: { + id: configId ?? undefined, + startTime: startTime ?? undefined, + endTime: endTime ?? undefined, + }, + skip: offset ?? undefined, + take: limit ?? undefined, + }); + } + + async updateSurveyConfigById( + surveyConfigId: number, + updateSurveyConfig: UpdateSurveyConfigDto, + filepath = null + ) { + let updatedConfig; + let userIdList: Set = new Set(); + + const surveyConfig = await this.prisma.surveyConfig.findUnique({ + where: { + id: surveyConfigId, + }, + }); + if (!surveyConfig) { + throw new NotFoundException( + `Survey config with ID ${surveyConfigId} not found.` + ); + } + if (isDateInPast(new Date(surveyConfig.startTime), new Date())) { + throw new NotAcceptableException( + `Cannot update a survey config after the startTime has already passed.` + ); + } + + try { + await this.prisma.$transaction(async (prismaClient) => { + if (filepath != null) { + const parsedData = await this.fileUploadService.parseCSV(filepath); + + await prismaClient.userMapping.deleteMany({ + where:{ + surveyConfigId + } + }); + + for (const obj of parsedData) { + obj.assessorIds = obj.assessorIds.replace(/\s/g, '').split(","); + + const userMapping: UserMappingFileUploadDto = obj; + + try { + userIdList = await this.userMetadataService.validateAndFetchUserIds( + prismaClient, + userIdList, + userMapping + ); + } catch (error) { + throw error; + } + + await prismaClient.userMapping.create({ + data: { + surveyConfigId, + assesseeId: userMapping.assesseeId, + assessorIds: userMapping.assessorIds, + }, + }); + } + + await this.fileUploadService.deleteUploadedFile(filepath); + } + + updatedConfig = await this.prisma.surveyConfig.update({ + where: { + id: surveyConfigId, + }, + data: updateSurveyConfig, + }); + }); + return updatedConfig; + } catch (error) { + if (filepath) { + await this.fileUploadService.deleteUploadedFile(filepath); + } + throw error; + } + } + + async deleteSurveyConfig(surveyConfigId: number) { + // Find the survey config with the provided Id and check it exists or not before deleting + const isExisted = await this.prisma.surveyConfig.findFirst({ + where: { + id: surveyConfigId, + }, + }); + if (!isExisted) { + throw new NotFoundException( + `The survey Config ID ${surveyConfigId} doesn't exist.` + ); + } + // Delete a single survey configuration based on its unique identifier (id). + const deletedSurveyConfig = await this.prisma.surveyConfig.delete({ + where: { + id: surveyConfigId, + }, + }); + return deletedSurveyConfig; + } + + async deactivateSurveyConfig(surveyConfigId: number) { + return await this.prisma.surveyConfig.update({ + where: { + id: surveyConfigId, + }, + data: { + isActive: false + }, + }); + } + + public async getUserMappingSampleForSurveyConfig(): Promise { + return [ + { + assesseeId: "8a98a623-2ad4-40af-bfb3-1a53051e89e9", + assessorIds: "8e11986e-36d4-4f7d-bf5e-b436a25ee661, 53fc7f0c-8724-4cba-80b9-3f6040e3aeae", + }, + { + assesseeId: "8e11986e-36d4-4f7d-bf5e-b436a25ee661", + assessorIds: "8a98a623-2ad4-40af-bfb3-1a53051e89e9, 53fc7f0c-8724-4cba-80b9-3f6040e3aeae", + }, + { + assesseeId: "53fc7f0c-8724-4cba-80b9-3f6040e3aeae", + assessorIds: "8a98a623-2ad4-40af-bfb3-1a53051e89e9, 8e11986e-36d4-4f7d-bf5e-b436a25ee661", + }, + ]; + } +} diff --git a/src/survey-form/dto/create-survey-form.dto.ts b/src/survey-form/dto/create-survey-form.dto.ts new file mode 100644 index 0000000..38506a6 --- /dev/null +++ b/src/survey-form/dto/create-survey-form.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { SurveyStatusEnum } from "@prisma/client"; +import { Type } from "class-transformer"; +import { + IsArray, + IsEnum, + IsNotEmpty, + IsNumber, + IsString, + IsUUID, + ValidateNested, +} from "class-validator"; + +export class responseObject { + @IsNumber() + @IsNotEmpty() + questionId: number; + + @IsString() + @IsNotEmpty() + question: string; +} +export class CreateSurveyFormDto { + @ApiProperty({ type: "integer", example: 1 }) + @IsUUID() + @IsNotEmpty() + userId: string; + + @IsNumber() + @IsNotEmpty() + surveyConfigId: number; + + @ApiProperty({ enum: SurveyStatusEnum, example: "CREATED" }) + @IsEnum(SurveyStatusEnum) + @IsNotEmpty() + status: SurveyStatusEnum; + + @ApiProperty({ + type: "array", + example: [{ questionId: 1, question: "Is this a dummy question?" }], + }) + @IsArray() + @IsNotEmpty() + @ValidateNested({ each: true }) + @Type(() => responseObject) + questionsJson: responseObject[]; +} diff --git a/src/survey-form/dto/index.ts b/src/survey-form/dto/index.ts new file mode 100644 index 0000000..5ed2c9e --- /dev/null +++ b/src/survey-form/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-survey-form.dto'; +export * from './update-survey-form.dto'; +export * from './response-survey-form.dto'; \ No newline at end of file diff --git a/src/survey-form/dto/response-survey-form.dto.ts b/src/survey-form/dto/response-survey-form.dto.ts new file mode 100644 index 0000000..99f73c9 --- /dev/null +++ b/src/survey-form/dto/response-survey-form.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { SurveyStatusEnum } from "@prisma/client"; +import { JsonValue } from "@prisma/client/runtime/library"; +import { SurveyScoreResponse } from "src/survey-score/dto"; +import { ResponseSurveyConfigDto } from "../../survey-config/dto/response-survey-config.dto"; +import { ResponseTracker } from "../../response-tracker/dto"; + +export class ResponseSurveyFormDto { + readonly id?: number; + readonly userId?: string; + readonly surveyConfigId?: number; + readonly SurveyConfig?: ResponseSurveyConfigDto; + readonly status?: SurveyStatusEnum; + readonly questionsJson?: JsonValue; + readonly overallScore?: number | null; + readonly sunbirdCredentialIds?: string | null; + readonly ResponseTracker?: ResponseTracker +} + +export class SurveyFormResponse { + readonly data?: ResponseSurveyFormDto; + readonly message: string; +} + +export class SurveyScoresForUser { + id: number; + @ApiProperty({ enum: SurveyStatusEnum, example: SurveyStatusEnum.CLOSED }) + status: SurveyStatusEnum; + userId: string; + overallScore: number | null; + sunbirdCredentialIds: string | null; + SurveyScore: SurveyScoreResponse[]; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/survey-form/dto/update-survey-form.dto.ts b/src/survey-form/dto/update-survey-form.dto.ts new file mode 100644 index 0000000..7ed34d5 --- /dev/null +++ b/src/survey-form/dto/update-survey-form.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { SurveyStatusEnum } from "@prisma/client"; +import { + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from "class-validator"; + +export class UpdateSurveyFormDto { + @ApiProperty({ enum: SurveyStatusEnum, example: "CREATED" }) + @IsEnum(SurveyStatusEnum) + @IsNotEmpty() + status: SurveyStatusEnum; + + @IsNumber() + @IsOptional() + @IsNotEmpty() + overallScore: number; + + @IsString() + @IsOptional() + @IsNotEmpty() + sunbirdCredentialIds: string; +} + +export class UpdateSurveyFormStatusDto { + @ApiProperty({ enum: SurveyStatusEnum, example: "CREATED" }) + @IsEnum(SurveyStatusEnum) + @IsNotEmpty() + status: SurveyStatusEnum; +} diff --git a/src/survey-form/survey-form.controller.spec.ts b/src/survey-form/survey-form.controller.spec.ts new file mode 100644 index 0000000..906ef70 --- /dev/null +++ b/src/survey-form/survey-form.controller.spec.ts @@ -0,0 +1,141 @@ +import * as pactum from "pactum"; +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { Test, TestingModule } from "@nestjs/testing"; +import { ConfigService } from "@nestjs/config"; +import { SurveyFormModule } from "./survey-form.module"; +import { CreateSurveyFormDto } from "./dto"; +import { SurveyStatusEnum } from "@prisma/client"; + +describe("SurveyFormController e2e", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [SurveyFormModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + + afterAll(async () => { + await app.close(); + }); + + const testData = { + userId: "4f67ae5a-c2a2-4652-81cc-4d1471e1a158", + surveyFormId: 1, + surveyCycleParameterId: 1, + }; + + describe("createSurveyForm", () => { + it("should create a new survey form", async () => { + const createSurveyFormDto: CreateSurveyFormDto = { + userId: testData.userId, + surveyCycleParameterId: testData.surveyCycleParameterId, + status: SurveyStatusEnum.CREATED, + questionsJson: [ + { questionId: 1, question: "Is this a dummy question?" }, + ], + }; + + const response = await pactum + .spec() + .post("/survey-form") + .withHeaders("Content-Type", "application/json") + .withBody(createSurveyFormDto) + .expectStatus(201); + + const createdSurveyForm = JSON.parse(JSON.stringify(response.body)); + expect(createdSurveyForm).not.toBeNull(); + expect(createdSurveyForm.message).toEqual( + "SurveyForm created successfully" + ); + expect(createdSurveyForm.data).toHaveProperty("id") + expect(createdSurveyForm.data.userId).toEqual(createSurveyFormDto.userId); + expect(createdSurveyForm.data.surveyCycleParameterId).toEqual( + createSurveyFormDto.surveyCycleParameterId + ); + expect(createdSurveyForm.data.status).toEqual(createSurveyFormDto.status); + expect(createdSurveyForm.data.questionsJson[0].questionId).toEqual( + createSurveyFormDto.questionsJson[0].questionId + ); + expect(createdSurveyForm.data.questionsJson[0].question).toEqual( + createSurveyFormDto.questionsJson[0].question + ); + }); + }); + + describe("getSurveyFormById", () => { + it("should get a survey form by ID", async () => { + const response = await pactum + .spec() + .get(`/survey-form/${testData.surveyFormId}`) + .withPathParams({ + id: testData.surveyFormId, + }) + .expectStatus(200); + + const surveyForm = JSON.parse(JSON.stringify(response.body)); + expect(surveyForm.message).toBe("SurveyForm fetched successfully"); + expect(surveyForm.data.id).toEqual(testData.surveyFormId); + }); + }); + + describe("updateSurveyFormStatus", () => { + it("should update the status of a survey form", async () => { + const updateData = { status: SurveyStatusEnum.CLOSED }; + const response = await pactum + .spec() + .patch(`/survey-form/status/${testData.surveyFormId}`) + .withPathParams({ + id: testData.surveyFormId, + }) + .withJson(updateData) + .expectStatus(200); + + const updatedSurveyForm = JSON.parse(JSON.stringify(response.body)); + expect(updatedSurveyForm.message).toBe("SurveyForm updated successfully"); + expect(updatedSurveyForm.data.id).toEqual(testData.surveyFormId); + expect(updatedSurveyForm.data.status).toBe(updateData.status); + expect(updatedSurveyForm.data.userId).toBeDefined(); + }); + }); + + describe("deleteSurveyForm", () => { + it("should delete a survey form by ID", async () => { + const response = await pactum + .spec() + .delete(`/survey-form/${testData.surveyFormId}`) + .withPathParams({ + id : testData.surveyFormId + }) + .expectStatus(200); + + const deletedSurveyForm = JSON.parse(JSON.stringify(response.body)); + + expect(deletedSurveyForm.message).toBe("SurveyForm deleted successfully"); + expect(deletedSurveyForm.data.id).toBe(testData.surveyFormId); + expect(deletedSurveyForm.data.status).toBeDefined(); + expect(deletedSurveyForm.data.userId).toBeDefined(); + }); + }); +}); diff --git a/src/survey-form/survey-form.controller.ts b/src/survey-form/survey-form.controller.ts new file mode 100644 index 0000000..2b231c9 --- /dev/null +++ b/src/survey-form/survey-form.controller.ts @@ -0,0 +1,190 @@ +import { + Controller, + Post, + Body, + HttpStatus, + Get, + Param, + Patch, + Delete, + Res, + Logger, + ParseIntPipe, +} from "@nestjs/common"; +import { SurveyFormService } from "./survey-form.service"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { + CreateSurveyFormDto, + SurveyFormResponse, + UpdateSurveyFormDto, + UpdateSurveyFormStatusDto, +} from "./dto"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; + +@Controller("survey-form") +@ApiTags("survey-form") +export class SurveyFormController { + private readonly logger = new Logger(SurveyFormController.name); + constructor(private readonly surveyFormService: SurveyFormService) {} + + @Post() + @ApiOperation({ summary: "Create a Survey-Form" }) + @ApiResponse({ status: HttpStatus.CREATED, type: SurveyFormResponse }) + async createSurveyForm( + @Res() res, + @Body() createSurveyFormDto: CreateSurveyFormDto + ): Promise { + try { + this.logger.log(`Initiated creating new survey form`); + + const surveyForm = await this.surveyFormService.createSurveyForm( + createSurveyFormDto + ); + + this.logger.log(`Successfully created new survey form`); + + return res.status(HttpStatus.CREATED).json({ + data: surveyForm, + message: "SurveyForm created successfully", + }); + } catch (error) { + this.logger.error(`Failed to create new survey form`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res + .status(statusCode) + .json({ message: errorMessage || "Could not create survey form" }); + } + } + + @Get(":id") + @ApiOperation({ summary: "Fetch a Survey-Form" }) + @ApiResponse({ status: HttpStatus.FOUND, type: SurveyFormResponse }) + async findSurveyFormById( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated fetching new survey form`); + + const surveyForm = await this.surveyFormService.findSurveyFormById(id); + + this.logger.log(`Successfully fetched the survey form`); + + return res.status(HttpStatus.OK).send({ + data: surveyForm, + message: "SurveyForm fetched successfully", + }); + } catch (error) { + this.logger.error(`Failed to fetch survey form`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res + .status(statusCode) + .send({ message: errorMessage || "Could not find survey form" }); + } + } + + @Patch("status/:id") + @ApiOperation({ summary: "Update the Survey-Form" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyFormResponse }) + async updateSurveyFormStatus( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateSurveyFormDto: UpdateSurveyFormStatusDto + ): Promise { + try { + this.logger.log(`Initiated updating new survey form`); + + const surveyForm = await this.surveyFormService.updateSurveyFormStatus( + id, + updateSurveyFormDto.status + ); + + this.logger.log(`Successfully updated the survey form`); + + return res.status(HttpStatus.OK).json({ + data: surveyForm, + message: "SurveyForm updated successfully", + }); + } catch (error) { + this.logger.error(`Failed to update survey form`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res + .status(statusCode) + .json({ message: errorMessage || "Could not update survey form" }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a Survey-Form" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyFormResponse }) + async deleteSurveyForm( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated deleting new survey form`); + + const surveyForm = await this.surveyFormService.deleteSurveyForm(id); + + this.logger.log(`Successfully deleted survey form`); + + return res.status(HttpStatus.OK).json({ + data: surveyForm, + message: "SurveyForm deleted successfully", + }); + } catch (error) { + this.logger.error(`Failed to delete survey form`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res + .status(statusCode) + .json({ message: errorMessage || "Could not delete survey form." }); + } + } + + @Get("latest-survey-form/:userId") + @ApiOperation({ summary: "Fetch latest Survey-Form by userId" }) + @ApiResponse({ status: HttpStatus.FOUND, type: SurveyFormResponse }) + async fetchLatestSurveyFormByUserId( + @Res() res, + @Param("userId") userId: string + ): Promise { + try { + this.logger.log( + `Initiated fetching latest Survey-Form by userId #${userId}` + ); + + const surveyForm = + await this.surveyFormService.fetchLatestSurveyFormByUserId(userId); + + this.logger.log( + `Successfully fetched latest Survey-Form by userId #${userId}` + ); + + return res.status(HttpStatus.OK).send({ + data: surveyForm, + message: `Successfully fetched latest Survey-Form by userId #${userId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch latest Survey-Form by userId #${userId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res.status(statusCode).send({ + message: + errorMessage || + `Could not find latest Survey-Form by userId #${userId}`, + }); + } + } +} diff --git a/src/survey-form/survey-form.module.ts b/src/survey-form/survey-form.module.ts new file mode 100644 index 0000000..53a5d56 --- /dev/null +++ b/src/survey-form/survey-form.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { SurveyFormController } from "./survey-form.controller"; +import { SurveyFormService } from "./survey-form.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; + +@Module({ + controllers: [SurveyFormController], + providers: [SurveyFormService, PrismaService], + exports:[SurveyFormService] +}) +export class SurveyFormModule {} diff --git a/src/survey-form/survey-form.service.ts b/src/survey-form/survey-form.service.ts new file mode 100644 index 0000000..a669e6f --- /dev/null +++ b/src/survey-form/survey-form.service.ts @@ -0,0 +1,187 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { SurveyStatusEnum } from "@prisma/client"; +import { PrismaService } from "../prisma/prisma.service"; +import { CreateSurveyFormDto, SurveyScoresForUser } from "./dto"; +import _ from "lodash"; + +@Injectable() +export class SurveyFormService { + constructor(private prisma: PrismaService) {} + + async createSurveyForm(createSurveyFormDto: CreateSurveyFormDto) { + const surveyFormDto = { + ...createSurveyFormDto, + questionsJson: JSON.parse( + JSON.stringify(createSurveyFormDto.questionsJson) + ), + }; + + return await this.prisma.surveyForm.create({ + data: surveyFormDto, + }); + } + + async findSurveyFormById(id: number) { + const surveyForm = await this.prisma.surveyForm.findUnique({ + where: { id }, + }); + if (!surveyForm) { + throw new NotFoundException("Survey Form not found"); + } + return surveyForm; + } + + async updateSurveyFormStatus(id: number, status: SurveyStatusEnum) { + return await this.prisma.surveyForm.update({ + where: { id }, + data: { status }, + }); + } + + async findSurveyFormBySurveyConfigId(configId: number) { + const surveyForms = await this.prisma.surveyForm.findMany({ + where: { + surveyConfigId: configId, + }, + }); + if (!surveyForms || surveyForms.length == 0) { + throw new NotFoundException( + `Survey Forms not found for SurveyConfig with id #${configId}` + ); + } + return surveyForms; + } + + async updateSurveyFormScore(id: number, overallScore: number) { + return await this.prisma.surveyForm.update({ + where: { id }, + data: { overallScore }, + }); + } + + async deleteSurveyForm(id: number) { + return await this.prisma.surveyForm.delete({ + where: { id }, + }); + } + + public async getAllSurveyFormScoresByUserId( + userId: string + ): Promise { + const userMetadata = await this.prisma.userMetadata.findUnique({ + where: { userId }, + }); + + if (!userMetadata) + throw new NotFoundException(`User with id #${userId} not found`); + + const response = await this.prisma.surveyForm.findMany({ + where: { + userId, + }, + select: { + id: true, + userId: true, + status: true, + overallScore: true, + sunbirdCredentialIds: true, + SurveyScore: true, + createdAt: true, + updatedAt: true, + }, + }); + + return response; + } + + public async getLatestSurveyFormScoresByUserId( + userId: string + ): Promise { + const userMetadata = await this.prisma.userMetadata.findUnique({ + where: { userId }, + }); + + if (!userMetadata) + throw new NotFoundException(`User with id #${userId} not found`); + + const latestSurveyForm = await this.prisma.surveyForm.findMany({ + where: { + userId, + status: SurveyStatusEnum.CLOSED, // Get score for only closed survey form + }, + orderBy: { + createdAt: "desc", // Order by createdAt in descending order to get the latest survey form + }, + select: { + id: true, + status: true, + userId: true, + overallScore: true, + sunbirdCredentialIds: true, + SurveyScore: true, + createdAt: true, + updatedAt: true, + }, + take: 1, // Limit the query to return only one result + }); + + return latestSurveyForm[0]; + } + + async fetchLatestSurveyFormByUserId(userId: string) { + return await this.prisma.surveyForm.findFirst({ + where: { + userId, + status: SurveyStatusEnum.PUBLISHED, + SurveyConfig: { + isActive: true, + }, + }, + select: { + id: true, + questionsJson: true, + UserMetadata: { + select: { + userId: true, + designation: true, + userName: true, + profilePicture: true, + }, + }, + }, + }); + } + + async fetchLatestSurveyScoreByUserId(userId: string): Promise { + const latestScore = await this.prisma.surveyForm.findMany({ + where: { + userId, + status: SurveyStatusEnum.CLOSED, + SurveyConfig: { + isActive: false, + }, + }, + orderBy:{ + createdAt: "desc" + }, + take: 1, + select: { + overallScore: true + }, + }); + if(_.isEmpty(latestScore) || (latestScore[0].overallScore != null && latestScore[0].overallScore<0)){ + return null; + } + return latestScore[0].overallScore; + } + + async updateOverallScoreAndCredentialDid(surveyFormId: number, overallScore: number|null, sunbirdCredentialIds: string){ + return await this.prisma.surveyForm.update({ + where:{id: surveyFormId}, + data:{ + overallScore, + sunbirdCredentialIds + } + }) + } +} diff --git a/src/survey-score/dto/create-survey-score.dto.ts b/src/survey-score/dto/create-survey-score.dto.ts new file mode 100644 index 0000000..2399849 --- /dev/null +++ b/src/survey-score/dto/create-survey-score.dto.ts @@ -0,0 +1,25 @@ +import { IsInt, IsNotEmpty, IsNumber } from "class-validator"; + +export class CreateSurveyScoreDto { + @IsNotEmpty() + @IsInt() + surveyFormId: number; + + @IsNotEmpty() + @IsInt() + competencyId: number; + + @IsNotEmpty() + @IsInt() + competencyLevelNumber: number; + + @IsNotEmpty() + @IsNumber() + score: number; +} + +export class GenerateSurveyScoreDto { + @IsNotEmpty() + @IsInt() + surveyFormId: number; +} diff --git a/src/survey-score/dto/index.ts b/src/survey-score/dto/index.ts new file mode 100644 index 0000000..8c1faec --- /dev/null +++ b/src/survey-score/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./create-survey-score.dto"; +export * from "./response-survey-score.dto"; +export * from "./update-survey-score.dto"; diff --git a/src/survey-score/dto/response-survey-score.dto.ts b/src/survey-score/dto/response-survey-score.dto.ts new file mode 100644 index 0000000..06a7cd3 --- /dev/null +++ b/src/survey-score/dto/response-survey-score.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { SurveyStatusEnum } from "@prisma/client"; + +export class SurveyScoreResponse { + readonly id: number; + readonly surveyFormId: number; + readonly competencyId: number; + readonly competencyLevelNumber: number; + readonly score: number; +} + +export class ResponseMessageDto { + statusCode?: number; + message: string; +} + +export class SurveyScoreResponseDto extends ResponseMessageDto { + data?: SurveyScoreResponse; +} + +export class SurveyScoreMultipleResponseDto { + data?: SurveyScoreResponse[]; + message: string; + statusCode?: number; +} + +export class SurveyScoresForUser { + surveyFormId: number; + @ApiProperty({ enum: SurveyStatusEnum, example: SurveyStatusEnum.CLOSED }) + status: SurveyStatusEnum; + userId: string; + overallScore: number; + sunbirdCredentialIds: string; + SurveyScore: SurveyScoreResponse[]; + createdAt: Date; + updatedAt: Date; +} + +export class SurveyScoresForUserDto extends ResponseMessageDto { + data?: SurveyScoresForUser; +} + +export class AllSurveyScoresForUserDto extends ResponseMessageDto { + data?: SurveyScoresForUser[]; +} diff --git a/src/survey-score/dto/update-survey-score.dto.ts b/src/survey-score/dto/update-survey-score.dto.ts new file mode 100644 index 0000000..82dca82 --- /dev/null +++ b/src/survey-score/dto/update-survey-score.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateSurveyScoreDto } from './create-survey-score.dto'; + +export class UpdateSurveyScoreDto extends PartialType(CreateSurveyScoreDto) {} diff --git a/src/survey-score/interfaces/survey-score.interfaces.ts b/src/survey-score/interfaces/survey-score.interfaces.ts new file mode 100644 index 0000000..e6aa917 --- /dev/null +++ b/src/survey-score/interfaces/survey-score.interfaces.ts @@ -0,0 +1,10 @@ +export interface IAnswerScore { + score: number; + totalQuestions: number; +} + +export interface IGroupScoreData extends IAnswerScore { + competencyId: number; + competencyLevelNumber: number; + scorePercentage: number; +} diff --git a/src/survey-score/survey-score.controller.spec.ts b/src/survey-score/survey-score.controller.spec.ts new file mode 100644 index 0000000..f7e12a6 --- /dev/null +++ b/src/survey-score/survey-score.controller.spec.ts @@ -0,0 +1,253 @@ +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import * as pactum from "pactum"; +import { PrismaService } from "../prisma/prisma.service"; +import { SurveyScoreModule } from "./survey-score.module"; +import { + CreateSurveyScoreDto, + GenerateSurveyScoreDto, + UpdateSurveyScoreDto, +} from "./dto"; + +describe("SurveyFormController e2e", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [SurveyScoreModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("create()", () => { + it("should create a new survey score", async () => { + const createSurveyScoreDto: CreateSurveyScoreDto = { + competencyId: 1, + competencyLevelNumber: 10, + score: 30, + surveyFormId: 1, + }; + + const response = await pactum + .spec() + .post("/survey-score") + .withBody(createSurveyScoreDto) + .expectStatus(201); + + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual(`Successfully created survey score`); + expect(surveyScore.data).toHaveProperty("id"); + expect(surveyScore.data.competencyLevelNumber).toEqual( + createSurveyScoreDto.competencyLevelNumber + ); + expect(surveyScore.data.surveyFormId).toEqual( + createSurveyScoreDto.surveyFormId + ); + expect(surveyScore.data.score).toEqual(createSurveyScoreDto.score); + expect(surveyScore.data.competencyId).toEqual( + createSurveyScoreDto.competencyId + ); + }); + + it("should return a 400 error if the request body is invalid", async () => { + const createSurveyScoreDto = { + surveyFormId: null, + userId: "", + answers: {}, + }; + + const response = await pactum + .spec() + .post("/survey-score") + .withBody(createSurveyScoreDto) + .expectStatus(400); + const createdSurveyScore = JSON.parse(JSON.stringify(response.body)); + expect(createdSurveyScore.error).toEqual("Bad Request"); + }); + }); + + describe("findAll()", () => { + it("should get all survey scores", async () => { + const response = await pactum + .spec() + .get("/survey-score") + .expectStatus(200); + + const surveyScores = JSON.parse(JSON.stringify(response.body)); + expect(surveyScores.message).toEqual( + "Successfully fetched all survey scores" + ); + expect(surveyScores.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("findBySurveyFormId()", () => { + it("should get survey scores by survey form ID", async () => { + const surveyFormId = 1; + + const response = await pactum + .spec() + .get(`/survey-score/survey-form/${surveyFormId}`) + .withPathParams({ + id: surveyFormId, + }) + .expectStatus(200); + + const surveyScores = JSON.parse(JSON.stringify(response.body)); + expect(surveyScores.message).toEqual( + `Successfully fetched survey score with surveyFormId #${surveyFormId}` + ); + expect(surveyScores.data.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("findOne()", () => { + it("should get a survey score by ID", async () => { + const id = 1; + + const response = await pactum + .spec() + .get(`/survey-score/${id}`) + .withPathParams({ + id, + }) + .expectStatus(200); + + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully fetched survey score with id #${id}` + ); + expect(surveyScore.data.id).toEqual(id); + expect(surveyScore.data.surveyFormId).toBeDefined(); + expect(surveyScore.data.competencyId).toBeDefined(); + expect(surveyScore.data.competencyLevelNumber).toBeDefined(); + expect(surveyScore.data.score).toBeDefined(); + }); + }); + + describe("update()", () => { + it("should update a survey score by ID", async () => { + const id = 1; + const updateSurveyScoreDto: UpdateSurveyScoreDto = { + score: 87, + }; + const response = await pactum + .spec() + .patch(`/survey-score/${id}`) + .withPathParams({ + id, + }) + .withBody(updateSurveyScoreDto) + .expectStatus(200); + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully updated survey score with id #${id}` + ); + expect(surveyScore.data.id).toEqual(id); + expect(surveyScore.data.surveyFormId).toBeDefined(); + expect(surveyScore.data.competencyId).toBeDefined(); + expect(surveyScore.data.competencyLevelNumber).toBeDefined(); + expect(surveyScore.data.score).toBeDefined(); + }); + }); + + describe("remove()", () => { + it("should delete a survey score by ID", async () => { + const id = 1; + const response = await pactum + .spec() + .delete(`/survey-score/${id}`) + .withPathParams({ + id, + }) + .expectStatus(200); + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully deleted survey score with id #${id}` + ); + expect(surveyScore.data.id).toEqual(id); + expect(surveyScore.data.surveyFormId).toBeDefined(); + expect(surveyScore.data.competencyId).toBeDefined(); + expect(surveyScore.data.competencyLevelNumber).toBeDefined(); + expect(surveyScore.data.score).toBeDefined(); + }); + }); + + describe("generateSurveyScore()", () => { + it("should calculate survey score by survey form id", async () => { + const generateSurveyScoreDto: GenerateSurveyScoreDto = { + surveyFormId: 1, + }; + const response = await pactum + .spec() + .post("/survey-score/calculate-score") + .withHeaders({ + "Content-Type": "application/json", + }) + .withBody(generateSurveyScoreDto) + .expectStatus(200); + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully calculated survey score for surveyFormId #${generateSurveyScoreDto.surveyFormId}` + ); + }); + }); + + describe("getLatestSurveyScoreByUserId()", () => { + it("should get latest survey scores by user id", async () => { + const userId = "4f67ae5a-c2a2-4652-81cc-4d1471e1a158"; + const response = await pactum + .spec() + .get(`/survey-score/latest-survey-score/${userId}`) + .withPathParams({ + userId, + }) + .expectStatus(200); + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully fetched latest survey score for userId #${userId}` + ); + }); + }); + + describe("getAllSurveyScoreByUserId()", () => { + it("should get all survey scores by user id", async () => { + const userId = "4f67ae5a-c2a2-4652-81cc-4d1471e1a158"; + const response = await pactum + .spec() + .get(`/survey-score/latest-survey-score/${userId}`) + .withPathParams({ + userId, + }) + .expectStatus(200); + const surveyScore = JSON.parse(JSON.stringify(response.body)); + expect(surveyScore.message).toEqual( + `Successfully fetched latest survey score for userId #${userId}` + ); + }); + }); +}); diff --git a/src/survey-score/survey-score.controller.ts b/src/survey-score/survey-score.controller.ts new file mode 100644 index 0000000..60282f5 --- /dev/null +++ b/src/survey-score/survey-score.controller.ts @@ -0,0 +1,360 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Logger, + Param, + ParseIntPipe, + Patch, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { + AllSurveyScoresForUserDto, + CreateSurveyScoreDto, + GenerateSurveyScoreDto, + ResponseMessageDto, + SurveyScoreMultipleResponseDto, + SurveyScoreResponseDto, + SurveyScoresForUserDto, + UpdateSurveyScoreDto, +} from "./dto"; +import { SurveyScoreService } from "./survey-score.service"; + +@Controller("survey-score") +@ApiTags("survey-score") +export class SurveyScoreController { + private logger = new Logger(SurveyScoreController.name); + constructor(private readonly surveyScoreService: SurveyScoreService) {} + + @Post() + @ApiOperation({ summary: "create new survey score" }) + @ApiResponse({ status: HttpStatus.CREATED, type: SurveyScoreResponseDto }) + async create( + @Res() res, + @Body() createSurveyScoreDto: CreateSurveyScoreDto + ): Promise { + try { + this.logger.log(`Initiated creating new survey score`); + + const surveyScore = await this.surveyScoreService.create( + createSurveyScoreDto + ); + + this.logger.log(`Successfully created new survey score`); + + return res.status(HttpStatus.CREATED).json({ + data: surveyScore, + message: `Successfully created survey score`, + }); + } catch (error) { + this.logger.error(`Failed to create new survey score`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to create survey score`, + }); + } + } + + @Get() + @ApiOperation({ summary: "get all survey score" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoreMultipleResponseDto }) + async findAll(@Res() res): Promise { + try { + this.logger.log(`Initiated fetching all survey scores`); + + const surveyScore = await this.surveyScoreService.findAll(); + + this.logger.log(`Successfully fetched all survey scores`); + + return res.status(HttpStatus.OK).json({ + data: surveyScore, + message: `Successfully fetched all survey scores`, + }); + } catch (error) { + this.logger.error(`Failed to fetch all survey score`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch all survey score`, + }); + } + } + + @Get("survey-form/:surveyFormId") + @ApiOperation({ summary: "get survey scores by surveyFormId" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoreMultipleResponseDto }) + async findBySurveyFormId( + @Res() res, + @Param("surveyFormId", ParseIntPipe) surveyFormId: number + ): Promise { + try { + this.logger.log( + `Initiated fetching survey score with surveyFormId #${surveyFormId}` + ); + + const surveyScore = await this.surveyScoreService.findBySurveyFormId( + surveyFormId + ); + + this.logger.log( + `Successfully fetched survey score with surveyFormId #${surveyFormId}` + ); + + return res.status(HttpStatus.OK).json({ + data: surveyScore, + message: `Successfully fetched survey score with surveyFormId #${surveyFormId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch survey score for surveyFormId #${surveyFormId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch survey score for surveyFormId #${surveyFormId}`, + }); + } + } + + @Get(":id") + @ApiOperation({ summary: "get survey score by id" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoreResponseDto }) + async findOne( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated fetching survey score with id #${id}`); + + const surveyScore = await this.surveyScoreService.findOne(id); + + this.logger.log(`Successfully fetched survey score with id #${id}`); + + return res.status(HttpStatus.OK).json({ + data: surveyScore, + message: `Successfully fetched survey score with id #${id}`, + }); + } catch (error) { + this.logger.error(`Failed to fetch survey score for id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch survey score for id #${id}`, + }); + } + } + + @Patch(":id") + @ApiOperation({ summary: "update survey score by id" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoreResponseDto }) + async update( + @Res() res, + @Param("id", ParseIntPipe) id: number, + @Body() updateSurveyScoreDto: UpdateSurveyScoreDto + ): Promise { + try { + this.logger.log(`Initiated updating survey score with id #${id}`); + + const surveyScore = await this.surveyScoreService.update( + id, + updateSurveyScoreDto + ); + + this.logger.log(`Successfully updated survey score with id #${id}`); + + return res.status(HttpStatus.OK).json({ + data: surveyScore, + message: `Successfully updated survey score with id #${id}`, + }); + } catch (error) { + this.logger.error(`Failed to update survey score with id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to update survey score with id #${id} `, + }); + } + } + + @Delete(":id") + @ApiOperation({ summary: "delete survey score by id" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoreResponseDto }) + async remove( + @Res() res, + @Param("id", ParseIntPipe) id: number + ): Promise { + try { + this.logger.log(`Initiated deleting survey score with id #${id}`); + + const surveyScore = await this.surveyScoreService.remove(id); + + this.logger.log(`Successfully deleted survey score with id #${id}`); + + return res.status(HttpStatus.OK).json({ + data: surveyScore, + message: `Successfully deleted survey score with id #${id}`, + }); + } catch (error) { + this.logger.error(`Failed to delete survey score with id #${id}`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to delete survey score with id #${id} `, + }); + } + } + + @Post("calculate-score") + @ApiOperation({ summary: "calculate survey score by survey form id" }) + @ApiResponse({ status: HttpStatus.OK }) + async generateSurveyScore( + @Res() res, + @Body() generateSurveyScoreDto: GenerateSurveyScoreDto + ): Promise { + const { surveyFormId } = generateSurveyScoreDto; + try { + this.logger.log( + `Initiated calculating survey score for surveyFormId #${surveyFormId}` + ); + + await this.surveyScoreService.calculateSurveyScoreBySurveyFormId( + surveyFormId + ); + + this.logger.log( + `Successfully calculated survey score for surveyFormId #${surveyFormId}` + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully calculated survey score for surveyFormId #${surveyFormId}`, + }); + } catch (error) { + this.logger.error( + `Failed to calculate survey score for surveyFormId #${surveyFormId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to calculate survey score for surveyFormId #${surveyFormId} `, + }); + } + } + + @Get("latest-survey-score/:userId") + @ApiOperation({ summary: "get latest survey score for user by user id" }) + @ApiResponse({ status: HttpStatus.OK, type: SurveyScoresForUserDto }) + async getLatestSurveyScoreByUserId( + @Res() res, + @Param("userId") userId: string + ): Promise { + try { + this.logger.log( + `Initiated fetching latest survey score for userId #${userId}` + ); + + const response = + await this.surveyScoreService.getLatestSurveyScoreByUserId(userId); + + this.logger.log( + `Successfully fetched latest survey score for userId #${userId}` + ); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched latest survey score for userId #${userId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch latest survey score for userId #${userId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch latest survey score for userId #${userId} `, + }); + } + } + + @Get("all-survey-score/:userId") + @ApiOperation({ summary: "get all surveys score for user by user id" }) + @ApiResponse({ status: HttpStatus.OK, type: AllSurveyScoresForUserDto }) + async getAllSurveyScoreByUserId( + @Res() res, + @Param("userId") userId: string + ): Promise { + try { + this.logger.log( + `Initiated fetching all survey score for userId #${userId}` + ); + + const response = await this.surveyScoreService.getAllSurveyScoreByUserId( + userId + ); + + this.logger.log( + `Successfully fetched all survey score for userId #${userId}` + ); + + return res.status(HttpStatus.OK).json({ + data: response, + message: `Successfully fetched all survey score for userId #${userId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch all survey score for userId #${userId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || + `Failed to fetch all survey score for userId #${userId} `, + }); + } + } +} diff --git a/src/survey-score/survey-score.module.ts b/src/survey-score/survey-score.module.ts new file mode 100644 index 0000000..da5f029 --- /dev/null +++ b/src/survey-score/survey-score.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; +import { ResponseTrackerService } from "../response-tracker/response-tracker.service"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import { QuestionBankModule } from "../question-bank/question-bank.module"; +import { SurveyScoreController } from "./survey-score.controller"; +import { SurveyScoreService } from "./survey-score.service"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [QuestionBankModule, PrismaModule], + controllers: [SurveyScoreController], + providers: [SurveyScoreService, ResponseTrackerService, SurveyFormService], +}) +export class SurveyScoreModule {} diff --git a/src/survey-score/survey-score.service.spec.ts b/src/survey-score/survey-score.service.spec.ts new file mode 100644 index 0000000..8590a58 --- /dev/null +++ b/src/survey-score/survey-score.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SurveyScoreService } from './survey-score.service'; + +describe('SurveyScoreService', () => { + let service: SurveyScoreService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SurveyScoreService], + }).compile(); + + service = module.get(SurveyScoreService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/survey-score/survey-score.service.ts b/src/survey-score/survey-score.service.ts new file mode 100644 index 0000000..8effe65 --- /dev/null +++ b/src/survey-score/survey-score.service.ts @@ -0,0 +1,336 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { ResponseTrackerStatusEnum, SurveyStatusEnum } from "@prisma/client"; +import { PrismaService } from "../prisma/prisma.service"; +import { QuestionBankService } from "../question-bank/question-bank.service"; +import { responseObject } from "../response-tracker/dto"; +import { AnswerEnum } from "../response-tracker/enums/response-tracker.enums"; +import { ResponseTrackerService } from "../response-tracker/response-tracker.service"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import _ from "lodash"; +import { CreateSurveyScoreDto } from "./dto/create-survey-score.dto"; +import { UpdateSurveyScoreDto } from "./dto/update-survey-score.dto"; +import { + IAnswerScore, + IGroupScoreData, +} from "./interfaces/survey-score.interfaces"; + +@Injectable() +export class SurveyScoreService { + constructor( + private prisma: PrismaService, + private responseTrackerService: ResponseTrackerService, + private questionBankService: QuestionBankService, + private surveyFormService: SurveyFormService + ) {} + + public async create(createSurveyScoreDto: CreateSurveyScoreDto) { + return await this.prisma.surveyScore.create({ data: createSurveyScoreDto }); + } + + public async findAll() { + return await this.prisma.surveyScore.findMany(); + } + + public async findOne(id: number) { + const surveyScore = await this.prisma.surveyScore.findUnique({ + where: { + id, + }, + }); + + if (!surveyScore) + throw new NotFoundException(`survey score with id #${id} not found`); + return surveyScore; + } + + public async findBySurveyFormId(surveyFormId: number) { + const surveyScore = await this.prisma.surveyScore.findMany({ + where: { surveyFormId }, + }); + if (!surveyScore || surveyScore.length === 0) + throw new NotFoundException( + `Survey score for survey form id #${surveyFormId} not found` + ); + return surveyScore; + } + + public async update(id: number, updateSurveyScoreDto: UpdateSurveyScoreDto) { + return await this.prisma.surveyScore.update({ + where: { id }, + data: updateSurveyScoreDto, + }); + } + + public async remove(id: number) { + return await this.prisma.surveyScore.delete({ where: { id } }); + } + + public async calculateSurveyScoreBySurveyFormId( + surveyFormId: number + ): Promise { + try { + const surveyForm = await this.surveyFormService.findSurveyFormById( + surveyFormId + ); + + if (surveyForm && surveyForm.status !== SurveyStatusEnum.CLOSED) { + throw new BadRequestException( + `Status for Survey form with id #${surveyFormId} is not ${SurveyStatusEnum.CLOSED}.` + ); + } + + const completedSurveyResponses = + await this.responseTrackerService.getAllResponseJsonBySurveyFormId( + surveyFormId, + ResponseTrackerStatusEnum.COMPLETED + ); + + const surveyResponses = _.compact( + _.flatten( + _.map(completedSurveyResponses, (response) => { + const result = _.get(response, "responseJson", []); + return !_.isEmpty(result) ? result : null; + }) + ) + ); + + if (_.isEmpty(surveyResponses)) { + const surveyQuestionIds = _.map( + JSON.parse(JSON.stringify(surveyForm.questionsJson)), + (question) => _.get(question, "questionId") + ); + + const emptyResponseScorePayload = await this.fetchEmptyResponseScores( + surveyQuestionIds, + surveyFormId + ); + + await this.prisma.surveyScore.createMany({ + data: emptyResponseScorePayload, + }); + + return `Successfully calculated score for survey form id #${surveyFormId}`; + } + + const answerScore = this.calculateAnswerScore(_.compact(surveyResponses)); + + const answerScoreAndCompetencyGroupData: { + [key: string]: IGroupScoreData; + } = await this.getAnswerScoreAndCompetencyGroupData(answerScore); + + const groupedScoreDataByCompetency = this.groupScoreDataByCompetency( + answerScoreAndCompetencyGroupData + ); + + const finalGroupedData = Object.values(groupedScoreDataByCompetency).map( + (group: IGroupScoreData) => ({ + ...group, + scorePercentage: Number( + ((group.score / group.totalQuestions || 0) * 100).toFixed(2) + ), + }) + ); + + const overallScore = + this.calculateOverAllScoreFromFinalGropedData(finalGroupedData); + + if (overallScore) { + await this.surveyFormService.updateSurveyFormScore( + surveyFormId, + overallScore + ); + } + + const scorePayload = this.formatScorePayloadFromFinalGroupDataAndFormId( + surveyFormId, + finalGroupedData + ); + + await this.prisma.surveyScore.createMany({ + data: scorePayload, + }); + + return `Successfully calculated score for survey form id #${surveyFormId}`; + } catch (error) { + throw error; + } + } + + public async fetchEmptyResponseScores( + questionIds: number[], + surveyFormId: number + ) { + return Promise.all( + questionIds.map(async (surveyQuestionId) => { + const question = await this.questionBankService.getQuestionById( + surveyQuestionId + ); + const { competencyId, competencyLevelNumber } = question; + return { + surveyFormId, + competencyId, + competencyLevelNumber, + score: -1, + }; + }) + ); + } + + public calculateAnswerScore(surveyResponses: responseObject[]): { + [key: string]: IAnswerScore; + } { + if (_.isEmpty(surveyResponses)) { + return {}; // Return an empty object when surveyResponses is empty + } + + const groupedResponses = _.groupBy(surveyResponses, "questionId"); + + const answerScores = _.mapValues(groupedResponses, (responses) => { + const totalQuestions = _.sumBy(responses, (response) => + response.answer === AnswerEnum.DO_NOT_KNOW ? 0 : 1 + ); + + const score = _.sumBy(responses, (response) => + response.answer === AnswerEnum.YES ? 1 : 0 + ); + + return { score, totalQuestions }; + }); + + return answerScores; + } + + public async getAnswerScoreAndCompetencyGroupData(answerScore: { + [key: string]: IAnswerScore; + }): Promise<{ [key: string]: IGroupScoreData }> { + if (_.isEmpty(answerScore)) { + return {}; // Return an empty object when answerScore is empty + } + + const questionIds = Object.keys(answerScore); + + // Use _.map to create an array of promises for each iteration + const promises = _.map(questionIds, async (questionId) => { + const question = await this.questionBankService.getQuestionById( + +questionId + ); + const { competencyId, competencyLevelNumber } = question; + + const scoreData = answerScore[questionId]; + + return [ + questionId, + { + score: scoreData.score, + totalQuestions: scoreData.totalQuestions, + competencyId, + competencyLevelNumber, + scorePercentage: 0, + }, + ]; + }); + + // Wait for all promises to resolve using Promise.all + const results = await Promise.all(promises); + + // Use _.fromPairs to transform the results array into an object + return _.fromPairs(results); + } + + public groupScoreDataByCompetency(groupedData: { + [key: string]: IGroupScoreData; + }): { [key: string]: IGroupScoreData } { + if (_.isEmpty(groupedData)) { + return {}; // Return an empty object when groupedData is empty + } + + return _.values(groupedData).reduce((acc, item) => { + const key = `${item.competencyId}-${item.competencyLevelNumber}`; + + acc[key] = acc[key] || { + score: 0, + totalQuestions: 0, + competencyId: item.competencyId, + competencyLevelNumber: item.competencyLevelNumber, + }; + + acc[key].score += item.score; + acc[key].totalQuestions += item.totalQuestions; + + return acc; + }, {}); + } + + public calculateOverAllScoreFromFinalGropedData( + finalGroupedData: IGroupScoreData[] + ): number | null { + if (_.isEmpty(finalGroupedData)) { + return null; // Return null default value for an empty payload + } + + const overallScore = _.reduce( + finalGroupedData, + (acc, group) => { + acc.score += group.score; + acc.totalQuestions += group.totalQuestions; + return acc; + }, + { score: 0, totalQuestions: 0 } + ); + + return Number( + ((overallScore.score / overallScore.totalQuestions) * 100).toFixed(2) + ); + } + + public formatScorePayloadFromFinalGroupDataAndFormId( + surveyFormId: number, + finalGroupedData: IGroupScoreData[] + ) { + if (_.isEmpty(finalGroupedData)) { + return []; // Return an empty array or any other appropriate default value for an empty payload + } + + return _.map(finalGroupedData, (data) => { + const { competencyId, competencyLevelNumber, scorePercentage } = data; + return { + surveyFormId, + competencyId, + competencyLevelNumber, + score: scorePercentage, + }; + }); + } + + public async getAllSurveyScoreByUserId(userId: string) { + const surveyScoreResponse = + await this.surveyFormService.getAllSurveyFormScoresByUserId(userId); + + if (!surveyScoreResponse) + throw new NotFoundException( + `Survey scores not found for user id #${userId}` + ); + + return _.map(surveyScoreResponse, (data) => { + const { id, ...rest } = data; + return { surveyFormId: id, ...rest }; + }); + } + + public async getLatestSurveyScoreByUserId(userId: string) { + const latestSurveyScore = + await this.surveyFormService.getLatestSurveyFormScoresByUserId(userId); + + if (!latestSurveyScore) + throw new NotFoundException( + `Survey score not found for user id #${userId}` + ); + + const { id, ...rest } = latestSurveyScore; + return { surveyFormId: id, ...rest }; + } +} diff --git a/src/survey/dto/index.ts b/src/survey/dto/index.ts new file mode 100644 index 0000000..d87d520 --- /dev/null +++ b/src/survey/dto/index.ts @@ -0,0 +1 @@ +export * from "./response-homescreen.dto"; \ No newline at end of file diff --git a/src/survey/dto/response-homescreen.dto.ts b/src/survey/dto/response-homescreen.dto.ts new file mode 100644 index 0000000..85c3a94 --- /dev/null +++ b/src/survey/dto/response-homescreen.dto.ts @@ -0,0 +1,12 @@ +import { SurveyDataResponse } from "../../response-tracker/dto"; +import { ResponseUserMetadataDto } from "../../user-metadata/dto/user-metadata-response.dto"; + +export class HomeScreenResponse{ + readonly users: ResponseUserMetadataDto[]; + readonly surveyData: SurveyDataResponse; +} + +export class HomeScreenAPIResponse { + readonly data: HomeScreenResponse; + readonly message: string; +} \ No newline at end of file diff --git a/src/survey/survey.controller.spec.ts b/src/survey/survey.controller.spec.ts new file mode 100644 index 0000000..f8fea70 --- /dev/null +++ b/src/survey/survey.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SurveyController } from './survey.controller'; + +describe('SurveyController', () => { + let controller: SurveyController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SurveyController], + }).compile(); + + controller = module.get(SurveyController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/survey/survey.controller.ts b/src/survey/survey.controller.ts new file mode 100644 index 0000000..fefabcc --- /dev/null +++ b/src/survey/survey.controller.ts @@ -0,0 +1,169 @@ +import { + Controller, + Get, + HttpStatus, + Logger, + Param, + ParseIntPipe, + ParseUUIDPipe, + Post, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; +import { SurveyService } from "./survey.service"; +import { ResponseSurveyFormDto, SurveyFormResponse } from "../survey-form/dto"; +import { HomeScreenAPIResponse } from "./dto"; + +@Controller("survey") +@ApiTags("survey") +export class SurveyController { + private readonly logger = new Logger(SurveyController.name); + constructor(private readonly surveyService: SurveyService) {} + + @Post(":configId") + @ApiOperation({ summary: "Create Survey-Forms for a SurveyConfig." }) + @ApiResponse({ status: HttpStatus.CREATED }) + async generateSurveyFormsForSurveyConfig( + @Res() res, + @Param("configId", ParseIntPipe) configId: number + ) { + try { + this.logger.log( + `Initiated creating new survey forms for a Survey Config.` + ); + + const surveyForms = + await this.surveyService.generateSurveyFormsForSurveyConfig(configId); + + this.logger.log( + `Successfully created new survey forms for a Survey Config.` + ); + + return res.status(HttpStatus.CREATED).json({ + data: surveyForms, + message: `SurveyForms created successfully for Survey Config with id #${configId}`, + }); + } catch (error) { + this.logger.error( + `Failed to create new survey forms for Survey Config with id #${configId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res.status(statusCode).json({ + message: + errorMessage || + `Could not create survey forms for Survey Config with id #${configId}`, + }); + } + } + + @Get("user-filled-survey/:userId") + @ApiOperation({ summary: "Get number of surveys to be filled by User." }) + @ApiResponse({ status: HttpStatus.OK, type: "number" }) + async getSurveysToBeFilledByUser( + @Res() res, + @Param("userId", ParseUUIDPipe) userId: string + ): Promise<{ data: number; message: string }> { + try { + this.logger.log( + `Getting number of surveys to be filled by user with id #${userId}.` + ); + + const surveyForms = await this.surveyService.getSurveysToBeFilledByUser( + userId + ); + + this.logger.log( + `Successfully fetched the number of surveys to be filled by user with id #${userId}.` + ); + + return res.status(HttpStatus.OK).json({ + data: surveyForms, + message: `Successfully fetched the number of surveys to be filled by user with id #${userId}.`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch the number of surveys to be filled by user with id #${userId}.`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res.status(statusCode).json({ + message: + errorMessage || + `Could not fetch the number of surveys to be filled by user with id #${userId}.`, + }); + } + } + + @Get("latest-survey-response/:userId") + @ApiOperation({ summary: "Fetch latest survey response by userId" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseSurveyFormDto }) + async getLatestSurveyResponsesForUserId( + @Res() res, + @Param("userId", ParseUUIDPipe) userId: string + ): Promise { + try { + this.logger.log( + `Initiated fetching latest survey response by userId #${userId}` + ); + + const surveyForm = + await this.surveyService.getLatestSurveyResponsesForUserId(userId); + + this.logger.log( + `Successfully fetched latest survey response by userId #${userId}` + ); + + return res.status(HttpStatus.OK).send({ + data: surveyForm, + message: `Successfully fetched latest survey response by userId #${userId}`, + }); + } catch (error) { + this.logger.error( + `Failed to fetch latest survey response by userId #${userId}`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res.status(statusCode).send({ + message: + errorMessage || + `Could not find latest survey response by userId #${userId}`, + }); + } + } + + @Get("home-screen") + @ApiOperation({ summary: "Fetch WPCAS home screen data" }) + @ApiResponse({ status: HttpStatus.OK, type: HomeScreenAPIResponse }) + async wpcasHomeScreenApi( + @Res() res, + ): Promise { + try { + this.logger.log(`Initiated fetching Home Screen data for WPCAS.`); + + const surveyForm = await this.surveyService.wpcasHomeScreenApi(); + + this.logger.log(`Successfully fetched Home Screen data for WPCAS.`); + + return res.status(HttpStatus.OK).send({ + data: surveyForm, + message: `Successfully fetched Home Screen data for WPCAS.`, + }); + } catch (error) { + this.logger.error(`Failed to fetch Home Screen data for WPCAS.`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + return res.status(statusCode).send({ + message: errorMessage || `Could not fetch Home Screen data for WPCAS.`, + }); + } + } +} diff --git a/src/survey/survey.module.ts b/src/survey/survey.module.ts new file mode 100644 index 0000000..b42ba2a --- /dev/null +++ b/src/survey/survey.module.ts @@ -0,0 +1,25 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AdminCompetencyModule } from "src/admin-competency/admin-competency.module"; +import { SurveyFormService } from "src/survey-form/survey-form.service"; +import { SurveyScoreService } from "src/survey-score/survey-score.service"; +import { QuestionBankModule } from "../question-bank/question-bank.module"; +import { ResponseTrackerService } from "../response-tracker/response-tracker.service"; +import { SurveyConfigModule } from "../survey-config/survey-config.module"; +import { SurveyFormModule } from "../survey-form/survey-form.module"; +import { UserMetadataModule } from "../user-metadata/user-metadata.module"; +import { SurveyController } from "./survey.controller"; +import { SurveyService } from "./survey.service"; + +@Module({ + imports: [ + QuestionBankModule, + SurveyFormModule, + forwardRef(() => UserMetadataModule), + forwardRef(() => SurveyConfigModule), + AdminCompetencyModule + ], + providers: [SurveyService, SurveyFormService, ResponseTrackerService, SurveyScoreService], + controllers: [SurveyController], + exports: [SurveyService], +}) +export class SurveyModule {} diff --git a/src/survey/survey.service.spec.ts b/src/survey/survey.service.spec.ts new file mode 100644 index 0000000..9cd098f --- /dev/null +++ b/src/survey/survey.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SurveyService } from './survey.service'; + +describe('SurveyService', () => { + let service: SurveyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SurveyService], + }).compile(); + + service = module.get(SurveyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/survey/survey.service.ts b/src/survey/survey.service.ts new file mode 100644 index 0000000..709a297 --- /dev/null +++ b/src/survey/survey.service.ts @@ -0,0 +1,278 @@ +import { + Inject, + Injectable, + NotAcceptableException, + NotFoundException, + forwardRef, +} from "@nestjs/common"; +import { AdminCompetency, ResponseTrackerStatusEnum, SurveyStatusEnum } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import { + CreateSurveyFormDto, + ResponseSurveyFormDto, +} from "src/survey-form/dto"; +import { SurveyFormService } from "src/survey-form/survey-form.service"; +import { QuestionBankService } from "../question-bank/question-bank.service"; +import _ from "lodash"; +import { UserMetadataService } from "../user-metadata/user-metadata.service"; +import { ResponseTrackerService } from "../response-tracker/response-tracker.service"; +import { HomeScreenResponse } from "./dto"; +import { SurveyConfigService } from "../survey-config/survey-config.service"; +import { SurveyScoreService } from "src/survey-score/survey-score.service"; +import { CompetencyCredentailDto, SurveyScoreCredentailDto } from "src/external-services/sunbird-rc/dto"; +import { AdminCompetencyService } from "src/admin-competency/admin-competency.service"; + +@Injectable() +export class SurveyService { + constructor( + private prisma: PrismaService, + private surveyForm: SurveyFormService, + private questionBank: QuestionBankService, + @Inject(forwardRef(() => UserMetadataService)) + private userMetadata: UserMetadataService, + private responseTracker: ResponseTrackerService, + private surveyConfig: SurveyConfigService, + private surveyScore: SurveyScoreService, + private adminCompetency: AdminCompetencyService + ) {} + + async getSurveysToBeFilledByUser(userId: string) { + const responseCount = await this.prisma.responseTracker.findMany({ + where: { + assessorId: userId, + status: ResponseTrackerStatusEnum.PENDING, + surveyForm: { + SurveyConfig: { + isActive: true, + }, + }, + }, + }); + if (!responseCount) { + return 0; + } + return responseCount.length; + } + + async getSurveysFilledByUser(userId: string) { + const responseCount = await this.prisma.responseTracker.findMany({ + where: { + assessorId: userId, + status: ResponseTrackerStatusEnum.COMPLETED, + surveyForm: { + SurveyConfig: { + isActive: true, + }, + }, + }, + }); + if (!responseCount) { + return 0; + } + return responseCount.length; + } + + async getLatestSurveyResponsesForUserId(userId: string) { + const responses = await this.prisma.surveyForm.findFirst({ + where: { + userId, + SurveyConfig: { + isActive: true, + }, + ResponseTracker: { + every: { + assesseeId: userId, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + select: { + ResponseTracker: { + select: { + id: true, + surveyFormId: true, + assesseeId: true, + Assessor: { + select: { + userId: true, + userName: true, + designation: true, + }, + }, + status: true, + }, + }, + }, + }); + if (!responses) { + throw new NotFoundException( + `For the user with id #${userId} either no responses found or no survey is active for their department.` + ); + } + return responses; + } + + async generateSurveyFormsForSurveyConfig(configId: number) { + let surveyForms: ResponseSurveyFormDto[] = []; + + const surveyConfig = await this.surveyConfig.getAllSurveyConfig({ + configId: configId, + }); + + if (surveyConfig[0].isActive == false) { + throw new NotAcceptableException( + `The survey with name: ${surveyConfig[0].surveyName} is not 'ACTIVE', thus cannot create the surveyForms.` + ); + } + + //logic to fetch user mapping + const userMapping = await this.prisma.userMapping.findMany({ + where: { + surveyConfigId: configId, + }, + }); + if (_.isUndefined(userMapping) || _.isEmpty(userMapping)) { + throw new NotFoundException( + `No user mapping found for Survey Config with id ${configId}` + ); + } + + for (const mapping of userMapping) { + //get questions for the user according to their designation + const questions = await this.questionBank.getAllQuestionsForUser( + mapping.assesseeId + ); + //create the CreateSurveyFormDto for the user + const surveyFormDto: CreateSurveyFormDto = { + userId: mapping.assesseeId, + surveyConfigId: configId, + status: SurveyStatusEnum.PUBLISHED, + questionsJson: questions, + }; + const surveyForm = await this.surveyForm.createSurveyForm(surveyFormDto); + surveyForms.push(surveyForm); + for (const assessor of mapping.assessorIds) { + if (mapping.assesseeId !== assessor) { + await this.prisma.responseTracker.create({ + data: { + surveyFormId: surveyForm.id, + assesseeId: mapping.assesseeId, + assessorId: assessor, + status: ResponseTrackerStatusEnum.PENDING, + }, + }); + } + } + } + return surveyForms; + } + + public async fetchUserIdsOfSurveyParticipants( + surveyConfigId: number + ): Promise> { + try { + let userIdList: Set = new Set(); + const userMappings = await this.prisma.userMapping.findMany({ + where: { surveyConfigId }, + }); + for (const userMapping of userMappings) { + const userValidationArray = [ + ...new Set([...userMapping.assessorIds, userMapping.assesseeId]), + ]; + userIdList = new Set([...userIdList, ...userValidationArray]); + } + return userIdList; + } catch (error) { + throw error; + } + } + + public async wpcasHomeScreenApi(): Promise { + try { + const users = await this.userMetadata.findManyUserMetadata({}); + const surveyData = await this.responseTracker.fetchActiveSurveyFormData(); + return { users, surveyData }; + } catch (error) { + throw error; + } + } + + public async createCredentialSchemaBySurveyFormId(surveyFormId: number) { + try { + let returnObj: SurveyScoreCredentailDto = {} as SurveyScoreCredentailDto; + let competencies: AdminCompetency[] = [] as AdminCompetency[]; + + const surveyForm = await this.surveyForm.findSurveyFormById( + surveyFormId + ); + const surveyScores = await this.surveyScore.findBySurveyFormId( + surveyFormId + ); + + const distinctCompetencyIds = _.uniqBy(surveyScores, 'competencyId').map(obj => obj.competencyId); + for(const competencyId of distinctCompetencyIds){ + const competency = await this.adminCompetency.findOne(competencyId); + competencies.push(competency); + } + + returnObj.userId = surveyForm.userId; + returnObj.dateOfSurveyScore = surveyForm.createdAt.toISOString(); + returnObj.overallScore = surveyForm.overallScore; + returnObj.competencies = await this.transformData(surveyScores, competencies); + + return returnObj; + } catch (error) { + throw error; + } + } + + public async transformData(scores, competencies) { + const result= [] as CompetencyCredentailDto[]; + + // Group scores by surveyFormId and competencyId + const groupedScores = _.groupBy(scores, 'competencyId'); + + // Iterate through competencies and transform the data + competencies.forEach((competency) => { + const competencyId = competency.id; + const competencyName = competency.name; + + const levels = competency.competencyLevels.map((level) => { + const levelNumber = level.competencyLevelNumber; + const levelName = level.competencyLevelName; + + // Find scores for the current competency and level + const competencyScores = groupedScores[competencyId] || []; + const matchingScore = competencyScores.find((score) => score.competencyLevelNumber === levelNumber); + + // Calculate score percentage + const scorePercentage = matchingScore ? matchingScore.score.toFixed(2) : null; + if(scorePercentage != 'N/A'){ + return { + levelNumber, + name: levelName, + score: scorePercentage == -1 ? null : parseFloat(scorePercentage), + }; + } else { + return null; + } + }); + + const filteredLevels = _.compact(levels); + + // Add the transformed data to the result array + result.push({ + id: competencyId, + name: competencyName, + levels: filteredLevels, + } as CompetencyCredentailDto); + }); + + // Sort result array by competency ID + const sortedResult = _.sortBy(result, 'id'); + + return sortedResult; + } +} diff --git a/src/user-metadata/dto/index.ts b/src/user-metadata/dto/index.ts new file mode 100644 index 0000000..235cefb --- /dev/null +++ b/src/user-metadata/dto/index.ts @@ -0,0 +1 @@ +export * from './user-metadata-filter.dto' \ No newline at end of file diff --git a/src/user-metadata/dto/user-metadata-filter.dto.ts b/src/user-metadata/dto/user-metadata-filter.dto.ts new file mode 100644 index 0000000..f81af00 --- /dev/null +++ b/src/user-metadata/dto/user-metadata-filter.dto.ts @@ -0,0 +1,32 @@ +import { + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, +} from "class-validator"; + +export class UserMetadataFilterDto { + @IsBoolean() + @IsOptional() + @IsNotEmpty() + isNewEmployee?: boolean; + + @IsString() + @IsOptional() + @IsNotEmpty() + designation?: string; + + @IsBoolean() + @IsOptional() + @IsNotEmpty() + isAdmin?: boolean; + + @IsInt() + @IsOptional() + limit?: number; + + @IsInt() + @IsOptional() + offset?: number; +} diff --git a/src/user-metadata/dto/user-metadata-response.dto.ts b/src/user-metadata/dto/user-metadata-response.dto.ts new file mode 100644 index 0000000..b080139 --- /dev/null +++ b/src/user-metadata/dto/user-metadata-response.dto.ts @@ -0,0 +1,14 @@ +import { ResponseSurveyFormDto } from "src/survey-form/dto"; + +export class ResponseUserMetadataDto { + readonly userId: string; + readonly designation?: string | null; + readonly isAdmin: boolean; + readonly isNewEmployee: boolean; + readonly dateOfJoining: Date; + readonly SurveyForms?: ResponseSurveyFormDto[]; +} + +export class UserResponseMessage { + message: string; +} diff --git a/src/user-metadata/user-metadata.controller.spec.ts b/src/user-metadata/user-metadata.controller.spec.ts new file mode 100644 index 0000000..2d5a9a7 --- /dev/null +++ b/src/user-metadata/user-metadata.controller.spec.ts @@ -0,0 +1,124 @@ +import * as pactum from "pactum"; +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import { UserMetadataModule } from "./user-metadata.module"; + +describe("UserMetadataController", () => { + let app: INestApplication; + let prisma: PrismaService; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [UserMetadataModule], + }).compile(); + + app = module.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + }) + ); + + const configService = app.get(ConfigService); + const apiPrefix = configService.get("API_PREFIX") || "api"; + const PORT = configService.get("APP_PORT") || 4010; + app.setGlobalPrefix(apiPrefix); + + await app.init(); + await app.listen(PORT); + + prisma = app.get(PrismaService); + pactum.request.setBaseUrl(`http://localhost:${PORT}/${apiPrefix}`); + }); + + afterAll(async () => { + await app.close(); + }); + + const userId1 = "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98"; + const userId2 = "4f67ae5a-c2a2-4652-81cc-4d1471e1a158"; + describe("UserMetadataController createOrUpdateUserMetadata()", () => { + it("should create or update user1 metadata", async () => { + + const response = await pactum + .spec() + .post("/user-metadata/" + userId1) + .expectStatus(200) + .expectBodyContains(userId1); + }); + }); + + describe("UserMetadataController createOrUpdateUserMetadata()", () => { + it("should create or update user2 metadata", async () => { + + const response = await pactum + .spec() + .post("/user-metadata/" + userId2) + .expectStatus(200) + .expectBodyContains(userId2); + }); + }); + + describe("UserMetadataController findUserMetadata()", () => { + it("should get user1 metadata by ID", async () => { + + const response = await pactum + .spec() + .get("/user-metadata/" + userId1) + .expectStatus(200) + .expectBodyContains(userId1); + }); + }); + + describe("UserMetadataController findManyUserMetadata()", () => { + it("should get all user metadata", async () => { + const response = await pactum + .spec() + .get("/user-metadata") + .expectStatus(200) + + const userMetadata = JSON.parse(JSON.stringify(response.body)); + expect(userMetadata.message).toEqual("UserMetadata(s) fetched successfully"); + expect(userMetadata.data.length).toBeGreaterThanOrEqual(0) + + }); + }); + + describe("UserMetadataController deleteUserMetadata()", () => { + it("should delete user1 metadata by ID", async () => { + + const response = await pactum + .spec() + .delete("/user-metadata/" + userId1) + .expectStatus(200) + .expectBodyContains(userId1); + }); + }); + + // describe("UserMetadataController deleteUserMetadata()", () => { + // it("should delete user2 metadata by ID", async () => { + + // const response = await pactum + // .spec() + // .delete("/user-metadata/" + userId2) + // .expectStatus(200) + // .expectBodyContains(userId2); + // }); + // }); + + // describe("UserMetadataController findManyUserMetadata()", () => { + // it("should get all user metadata", async () => { + // const response = await pactum + // .spec() + // .get("/user-metadata") + // .expectStatus(200) + + // const userMetadata = JSON.parse(JSON.stringify(response.body)); + // expect(userMetadata).toStrictEqual({"data": [], "message": "UserMetadata(s) fetched successfully"}); + + // }); + // }); +}); diff --git a/src/user-metadata/user-metadata.controller.ts b/src/user-metadata/user-metadata.controller.ts new file mode 100644 index 0000000..39878d9 --- /dev/null +++ b/src/user-metadata/user-metadata.controller.ts @@ -0,0 +1,199 @@ +import { + Controller, + Delete, + Get, + HttpStatus, + Logger, + Param, + ParseUUIDPipe, + Post, + Query, + Res, +} from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { getPrismaErrorStatusAndMessage } from "../utils/utils"; +import { UserMetadataFilterDto } from "./dto"; +import { + ResponseUserMetadataDto, + UserResponseMessage, +} from "./dto/user-metadata-response.dto"; +import { UserMetadataService } from "./user-metadata.service"; + +@Controller("user-metadata") +@ApiTags("user-metadata") +export class UserMetadataController { + constructor(private readonly userMetadataService: UserMetadataService) {} + private readonly logger = new Logger(UserMetadataController.name); + + @Post("sync-user-metadata") + @ApiOperation({ summary: "Sync userMetadata with user service" }) + @ApiResponse({ status: HttpStatus.CREATED, type: UserResponseMessage }) + async SyncUserDataWithFrac(@Res() res): Promise { + try { + this.logger.log(`Initiated sync of userMetadata with user service`); + + const userMetadata = + await this.userMetadataService.syncUserDataWithFrac(); + + this.logger.log(`Successfully sync userMetadata with user service.`); + + return res.status(HttpStatus.CREATED).json({ + message: "Successfully sync userMetadata with user service.", + data: userMetadata, + }); + } catch (error) { + this.logger.error(`Failed to sync userMetadata with user service.`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed sync userMetadata with user service.`, + }); + } + } + + @Post(":userId") + @ApiOperation({ summary: "Create or Update User Metadata" }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseUserMetadataDto }) + async createOrUpdateUserMetadata( + @Res() res, + @Param("userId", ParseUUIDPipe) userId: string + ): Promise { + try { + this.logger.log(`Creating or Updating User Metadata`); + + const userMetadata = + await this.userMetadataService.createOrUpdateUserMetadata(userId); + + this.logger.log(`Successfully created the new survey config.`); + + return res.status(HttpStatus.OK).json({ + message: "UserMetadata created or updated successfully", + data: userMetadata, + }); + } catch (error) { + this.logger.error(`Failed to Create or Update User Metadata.`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to Create or Update User Metadata.`, + }); + } + } + + @Get(":userId") + @ApiOperation({ summary: "Fetch User Metadata by userId" }) + @ApiResponse({ status: HttpStatus.FOUND, type: ResponseUserMetadataDto }) + async findUserMetadata( + @Res() res, + @Param("userId", ParseUUIDPipe) userId: string + ): Promise { + try { + this.logger.log(`Fetching User Metadata`); + + const userMetadata = await this.userMetadataService.findUserMetadataById( + userId + ); + + this.logger.log(`Successfully fetched UserMetadata for id #${userId}.`); + + return res.status(HttpStatus.OK).json({ + message: "UserMetadata fetched successfully", + data: userMetadata, + }); + } catch (error) { + this.logger.error( + `Failed to fetch UserMetadata for id #${userId}.`, + error + ); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: + errorMessage || `Failed to fetch UserMetadata for id #${userId}.`, + }); + } + } + + @Get() + @ApiOperation({ + summary: "Fetch User Metadata accordind to the filter specified.", + }) + @ApiResponse({ + status: HttpStatus.FOUND, + type: ResponseUserMetadataDto, + isArray: true, + }) + async findManyUserMetadata( + @Res() res, + @Query() filter: UserMetadataFilterDto + ): Promise { + try { + this.logger.log(`Fetching UserMetadata(s)`); + + const userMetadata = await this.userMetadataService.findManyUserMetadata( + filter + ); + + this.logger.log(`Successfully fetched UserMetadata(s).`); + + return res.status(HttpStatus.OK).json({ + message: "UserMetadata(s) fetched successfully", + data: userMetadata, + }); + } catch (error) { + this.logger.error(`Failed to fetch UserMetadata(s).`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch UserMetadata(s).`, + }); + } + } + + @Delete(":userId") + @ApiOperation({ summary: "Delete User Metadata." }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseUserMetadataDto }) + async deleteUserMetadata( + @Res() res, + @Param("userId", ParseUUIDPipe) userId: string + ): Promise { + try { + this.logger.log(`Deleting UserMetadata.`); + + const userMetadata = await this.userMetadataService.deleteUserMetadata( + userId + ); + + this.logger.log( + `Successfully deleted UserMetadata with userId ${userId}.` + ); + + return res.status(HttpStatus.OK).json({ + message: "UserMetadata deleted successfully", + data: userMetadata, + }); + } catch (error) { + this.logger.error(`Failed to delete UserMetadata.`, error); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to delete UserMetadata.`, + }); + } + } +} diff --git a/src/user-metadata/user-metadata.module.ts b/src/user-metadata/user-metadata.module.ts new file mode 100644 index 0000000..3ec0232 --- /dev/null +++ b/src/user-metadata/user-metadata.module.ts @@ -0,0 +1,20 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { MockUserService } from "src/mockModules/mock-user/mock-user.service"; +import { SurveyConfigModule } from "../survey-config/survey-config.module"; +import { UserMetadataController } from "./user-metadata.controller"; +import { UserMetadataService } from "./user-metadata.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { SurveyModule } from "../survey/survey.module"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import { TarentoService } from "src/external-services/tarento/tarento.service"; + +@Module({ + imports: [ + forwardRef(() => SurveyConfigModule), + forwardRef(() => SurveyModule), + ], + controllers: [UserMetadataController], + providers: [UserMetadataService, PrismaService, MockUserService, SurveyFormService, TarentoService], + exports: [UserMetadataService], +}) +export class UserMetadataModule {} diff --git a/src/user-metadata/user-metadata.service.ts b/src/user-metadata/user-metadata.service.ts new file mode 100644 index 0000000..f549084 --- /dev/null +++ b/src/user-metadata/user-metadata.service.ts @@ -0,0 +1,315 @@ +import { + Inject, + Injectable, + NotFoundException, + forwardRef, +} from "@nestjs/common"; +import { TimeUnitsEnum, UserRolesEnum } from "@prisma/client"; +import _ from "lodash"; +import { MockUserService } from "../mockModules/mock-user/mock-user.service"; +import { PrismaService } from "../prisma/prisma.service"; +import { SurveyConfigService } from "../survey-config/survey-config.service"; +import { UserMetadataFilterDto } from "./dto"; +import { UserMappingFileUploadDto } from "../survey-config/dto/create-survey-config.dto"; +import { SurveyService } from "../survey/survey.service"; +import { SurveyFormService } from "../survey-form/survey-form.service"; +import { TarentoService } from "src/external-services/tarento/tarento.service"; +import { convertToISODateTime } from "src/utils/utils"; + +@Injectable() +export class UserMetadataService { + constructor( + private prisma: PrismaService, + private mockUser: MockUserService, + @Inject(forwardRef(() => SurveyConfigService)) + private surveyConfig: SurveyConfigService, + @Inject(forwardRef(() => SurveyService)) + private surveyService: SurveyService, + private surveyForm: SurveyFormService, + private tarentoService: TarentoService + ) {} + + public async syncUserDataWithFrac() { + // fetch all users + let users = await this.tarentoService.getAllUsers(); + let userCount = 0; + + // update or create data in user metadata table + const usersMetaDataPayload = await Promise.all( + users.map(async (user) => { + try { + const isAdmin = user.organisations[0]?.roles.includes("ADMIN") ?? false; + + const designation = + user.profileDetails?.professionalDetails?.length > 0 + ? user.profileDetails?.professionalDetails[0]?.designation + : ""; + + const dateOfJoining = + user.profileDetails?.professionalDetails?.length > 0 + ? user.profileDetails?.professionalDetails[0]?.doj + : user.createdDate; + + const dateOfJoiningISO = await convertToISODateTime(dateOfJoining); + + const userMetadataPayload = { + userId: user.id, + userName: user.userName, + designation, + isNewEmployee: false, + dateOfJoining: dateOfJoiningISO, + isAdmin, + profilePicture: user?.profilePicture ?? "", + }; + + userCount++; + console.log("userData: ", { + userName: user.userName, + designation: designation, + userCount: userCount, + }); + + // Upsert user metadata in Prisma + return await this.prisma.userMetadata.upsert({ + where: { userId: userMetadataPayload.userId }, + create: userMetadataPayload, + update: userMetadataPayload, + }); + } catch (error) { + console.error(`Error upserting user metadata for user ID ${user.id}:`, error); + // Return null or any specific value to indicate that upsert failed for this user + return null; + } + }) + ); + + // Filter out any null values (failed upserts) from the results + const successfulUpserts = usersMetaDataPayload.filter( + (result) => result !== null + ); + + return successfulUpserts; + } + + async createOrUpdateUserMetadata(userId: string) { + let userMetadata = await this.prisma.userMetadata.findUnique({ + where: { userId }, + select: this.metadataSelect, + }); + // const user = await this.mockUser.findOne(userId); + let response = await this.tarentoService.getUser(userId); + + const user = { + id: response.data.result?.response?.content[0]?.id, + userName: response.data.result?.response?.content[0].userName, + createdAt: response.data.result?.response?.content[0]?.createdDate, + role: + response.data.result?.response?.content[0]?.organisations[0]?.roles.indexOf( + "ADMIN" + ) != -1 + ? "ADMIN" + : "", // user service has "roles" for every user + profilePicture: userMetadata?.profilePicture, + designation: + response.data.result?.response?.content[0]?.profileDetails?.professionalDetails?.length > 0 + ? response.data.result?.response?.content[0]?.profileDetails?.professionalDetails[0]?.designation + : "", + dateOfJoining: + response.data.result?.response?.content[0]?.profileDetails?.professionalDetails?.length > 0 + ? response.data.result?.response?.content[0]?.profileDetails?.professionalDetails[0]?.doj + : response.data.result?.response?.content[0]?.createdDate, + }; + + const userObj = { + userId: user.id, + userName: user.userName, + isNewEmployee: false, + dateOfJoining: user.dateOfJoining, + isAdmin: user.role == "ADMIN" ? true : false, + designation: user.designation, + profilePicture: null, // user service doesn't store profilePicture + }; + + if (!userMetadata) { + userMetadata = await this.prisma.userMetadata.create({ + data: userObj, + select: this.metadataSelect, + }); + } else { + if (userMetadata !== userObj) { + userMetadata = await this.prisma.userMetadata.update({ + where: { userId }, + data: { + isNewEmployee: false, + dateOfJoining: user.dateOfJoining, + isAdmin: user.role == UserRolesEnum.ADMIN ? true : false, + designation: user.designation, + profilePicture: user.profilePicture, + }, + select: this.metadataSelect, + }); + } + } + return userMetadata; + } + + async findUserMetadataById(userId: string) { + const userMetadata = await this.prisma.userMetadata.findUnique({ + where: { userId }, + select: this.metadataSelect, + }); + if (!userMetadata) + throw new NotFoundException( + `UserMetadata for user with id #${userId} not found` + ); + return userMetadata; + } + + async findManyUserMetadata(filter: UserMetadataFilterDto) { + const { isAdmin, isNewEmployee, designation, limit, offset } = filter; + let users = await this.prisma.userMetadata.findMany({ + where: { + isAdmin: isAdmin ?? undefined, + isNewEmployee: isNewEmployee ?? undefined, + designation: designation ?? undefined, + }, + skip: offset ?? undefined, + take: limit ?? undefined, + select: this.metadataSelect, + }); + for (const user of users) { + const surveysToBeFilled = + await this.surveyService.getSurveysToBeFilledByUser(user.userId); + const surveysFilled = await this.surveyService.getSurveysFilledByUser( + user.userId + ); + const wpcasScore = await this.surveyForm.fetchLatestSurveyScoreByUserId( + user.userId + ); + user["surveysToBeFilled"] = surveysToBeFilled; + user["surveysFilled"] = surveysFilled; + user["wpcasScore"] = wpcasScore; + } + + return users; + } + + async deleteUserMetadata(userId: string) { + return await this.prisma.userMetadata.delete({ + where: { userId }, + select: this.metadataSelect, + }); + } + + public async isEmployeeNew( + dateOfJoining: Date, + configId: number + ): Promise { + const surveyConfig = await this.surveyConfig.getAllSurveyConfig({ + configId, + }); + + if (surveyConfig.length == 0) return true; + + let dateToBeCompared = new Date(dateOfJoining); + const today = new Date(); + const { onboardingTime, onboardingTimeUnit } = surveyConfig[0]; + + if (onboardingTimeUnit === TimeUnitsEnum.DAY) { + dateToBeCompared.setDate(dateToBeCompared.getDate() + onboardingTime); + } else if (onboardingTimeUnit === TimeUnitsEnum.MONTH) { + dateToBeCompared.setMonth(dateToBeCompared.getMonth() + onboardingTime); + } else { + dateToBeCompared.setFullYear( + dateToBeCompared.getFullYear() + onboardingTime + ); + } + + return dateToBeCompared <= today; + } + + async getSurveyFormsByUserId(userId: string) { + const userMetadata = await this.prisma.userMetadata.findUnique({ + where: { + userId, + }, + select: { + userId: true, + dateOfJoining: true, + designation: true, + isNewEmployee: true, + SurveyForm: { + orderBy: { + createdAt: "asc", + }, + select: { + id: true, + surveyConfigId: true, + questionsJson: true, + status: true, + overallScore: true, + sunbirdCredentialIds: true, + ResponseTracker: { + select: { + id: true, + Assessor: { + select: { + userId: true, + userName: true, + }, + }, + responseJson: true, + status: true, + }, + }, + }, + }, + }, + }); + if (!userMetadata) + throw new NotFoundException( + `UserMetadata for user with id #${userId} not found` + ); + return userMetadata; + } + + public async validateAndFetchUserIds( + prismaClient, + userIdList: Set, + userMapping: UserMappingFileUploadDto + ) { + const userValidationArray = [ + ...new Set([...userMapping.assessorIds, userMapping.assesseeId]), + ]; + + const idsNotInUserIdList: string[] = userValidationArray.filter( + (id) => !userIdList.has(id) + ); + + for (const id of idsNotInUserIdList) { + try { + const user = await prismaClient.userMetadata.findUnique({ + where: { userId: id }, + }); + + if (!user) { + await this.createOrUpdateUserMetadata(id); + } + } catch (error) { + throw new NotFoundException(`User with id: ${id} not found.`); + } + } + + return (userIdList = new Set([...userIdList, ...idsNotInUserIdList])); + } + + metadataSelect = { + userId: true, + userName: true, + dateOfJoining: true, + isAdmin: true, + designation: true, + isNewEmployee: true, + profilePicture: true, + }; +} diff --git a/src/utils/custom-decorators/isTwoFieldsRequired.ts b/src/utils/custom-decorators/isTwoFieldsRequired.ts new file mode 100644 index 0000000..dbb7484 --- /dev/null +++ b/src/utils/custom-decorators/isTwoFieldsRequired.ts @@ -0,0 +1,38 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface, + } from "class-validator"; + + @ValidatorConstraint({ async: false }) + export class TwoFieldValidator implements ValidatorConstraintInterface { + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[relatedPropertyName]; + return (value && relatedValue) || (!value && !relatedValue); + } + + defaultMessage(args: ValidationArguments) { + return `Both '${args.property}' and '${args.constraints[0]}' must be given or both must be not given.`; + } + } + + export function IsTwoFieldsRequired( + propertyName: string, + relatedPropertyName: string, + validationOptions?: ValidationOptions + ) { + return function (object: Record, propertyName: string) { + registerDecorator({ + name: "isTwoFieldsRequired", + target: object.constructor, + propertyName, + constraints: [relatedPropertyName], + options: validationOptions, + validator: TwoFieldValidator, + }); + }; + } + \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7ecc693..0bb235a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,10 @@ +import { HttpStatus } from "@nestjs/common"; +import { + PrismaClientKnownRequestError, + PrismaClientValidationError, +} from "@prisma/client/runtime/library"; +import _ from "lodash"; + export const validationOptions = { whitelist: true, transform: true, @@ -6,3 +13,97 @@ export const validationOptions = { enableImplicitConversion: true, }, }; + +export function getPrismaErrorStatusAndMessage(error: any): { + errorMessage: string | undefined; + statusCode: number; +} { + if ( + error instanceof PrismaClientKnownRequestError || + error instanceof PrismaClientValidationError + ) { + const errorCode = _.get(error, "code", "DEFAULT_ERROR_CODE"); + + const errorCodeMap: Record = { + P2000: HttpStatus.BAD_REQUEST, + P2002: HttpStatus.CONFLICT, + P2003: HttpStatus.CONFLICT, + P2025: HttpStatus.NOT_FOUND, + DEFAULT_ERROR_CODE: HttpStatus.INTERNAL_SERVER_ERROR, + }; + + const statusCode = errorCodeMap[errorCode]; + const errorMessage = error.message.split("\n").pop(); + + return { statusCode, errorMessage }; + } + + const statusCode = + error?.status || + error?.response?.status || + HttpStatus.INTERNAL_SERVER_ERROR; + return { + statusCode, + errorMessage: error.message, + }; +} + +export function isTomorrow(inputDate: Date): boolean { + const currentDate = new Date(); + + const tomorrow = new Date(currentDate); + tomorrow.setDate(currentDate.getDate() + 1); + + tomorrow.setUTCHours(0, 0, 0, 0); + + inputDate.setUTCHours(0, 0, 0, 0); + + // Compare the input date with tomorrow + return inputDate.getTime() === tomorrow.getTime(); +} + +export function isDayBeforeToday(date) { + const today = new Date(); + today.setUTCHours(0, 0, 0, 0); + + const yesterday = new Date(date); + yesterday.setDate(yesterday.getDate() - 1); + + return yesterday.getTime() === today.getTime(); +} + +export function isToday(date) { + const today = new Date(); + today.setUTCHours(0, 0, 0, 0); + + const givenDate = new Date(date); + givenDate.setUTCHours(0, 0, 0, 0); + + return givenDate.getTime() === today.getTime(); +} + +export function isDateInPast(compareDate: Date, referenceDate: Date): boolean { + // Convert both dates to UTC to ensure accurate comparison + const compareDateUTC = new Date(compareDate.toUTCString()); + compareDateUTC.setUTCHours(0, 0, 0, 0); + const referenceDateUTC = new Date(referenceDate.toUTCString()); + referenceDateUTC.setUTCHours(0, 0, 0, 0); + + // Compare the dates + return compareDateUTC < referenceDateUTC; +} + +export function convertToISODateTime(dateString) { + // Create a new Date object from the input date string + const date = new Date(dateString); + + // Check if the date is invalid + if (isNaN(date.getTime())) { + return "Invalid Date"; + } + + // Convert the Date object to ISO-8601 DateTime format + const isoDateTime = date.toISOString(); + + return isoDateTime; +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..7207cd1 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,10 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] -} + "exclude": [ + "node_modules", + "test", + "dist", + "**/*spec.ts", + "prisma" + ] +} \ No newline at end of file