diff --git a/.commitlintrc b/.commitlintrc index 1e59a32dc..5cf1ccc1d 100644 --- a/.commitlintrc +++ b/.commitlintrc @@ -1,41 +1,38 @@ { - "extends": [ - "@commitlint/config-conventional" - ], - "rules": { - "body-max-line-length": [ - 0, - "always" + "extends": [ + "@commitlint/config-conventional" ], - "subject-case": [ - 2, - "always", - [ - "sentence-case", - "start-case", - "pascal-case", - "upper-case", - "lower-case", - "camel-case" - ] - ], - "type-enum": [ - 2, - "always", - [ - "build", - "chore", - "ci", - "docs", - "feat", - "fix", - "perf", - "refactor", - "revert", - "style", - "test", - "sample" - ] - ] - } + "rules": { + "body-max-line-length": [ + 0, + "always" + ], + "subject-case": [ + 2, + "always", + [ + "sentence-case", + "start-case", + "pascal-case", + "upper-case", + "lower-case", + "camel-case" + ] + ], + "type-enum": [ + 2, + "always", + [ + "ci", + "doc", + "feat", + "fix", + "hotfix", + "refactor", + "revert", + "style", + "test" + ] + ] + } } diff --git a/.env.example b/.env.example index ff16f8260..dc8cd0452 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME=ACK +APP_NAME=NestJS_ACK APP_ENV=development APP_LANGUAGE=en APP_TIMEZONE=Asia/Jakarta @@ -11,16 +11,13 @@ HTTP_PORT=3000 URL_VERSIONING_ENABLE=true URL_VERSION=1 -JOB_ENABLE=false - -DATABASE_URI=mongodb://admin:password@localhost:30001,localhost:30002,localhost:30003/ack?replicaSet=rs0&retryWrites=true&w=majority +DATABASE_URI=mongodb://host.docker.internal:27017,host.docker.internal:27018,host.docker.internal:27019/ack?retryWrites=true&w=majority&replicaSet=rs0 DATABASE_DEBUG=false -AUTH_JWT_SUBJECT=AckDevelopment AUTH_JWT_ISSUER=https://example.com AUTH_JWT_AUDIENCE=ack -AUTH_JWT_ACCESS_TOKEN_EXPIRED=1h +AUTH_JWT_ACCESS_TOKEN_EXPIRED=15m AUTH_JWT_ACCESS_TOKEN_SECRET_KEY=1234567890 AUTH_JWT_REFRESH_TOKEN_EXPIRED=182d AUTH_JWT_REFRESH_TOKEN_SECRET_KEY=0987654321 @@ -39,4 +36,11 @@ AWS_SES_CREDENTIAL_KEY= AWS_SES_CREDENTIAL_SECRET= AWS_SES_REGION=ap-southeast-3 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_TLS=false + SENTRY_DSN= + +CLIENT_URL=https://example.com \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 12800be33..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,15 +0,0 @@ - -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - day: tuesday - time: "00:00" - open-pull-requests-limit: 3 - target-branch: "development" - commit-message: - prefix: "github-action" - labels: - - dependabot diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 000000000..1529a462a --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,298 @@ +name: CI_CD +on: + push: + branches: + - main + - staging + workflow_dispatch: + +jobs: + build_image: + runs-on: ubuntu-latest + + env: + DOCKERFILE: ci/dockerfile + + DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKERHUB_REPO_NAME: ${{ vars.DOCKERHUB_REPO_NAME }} + + steps: + - name: Git checkout + uses: actions/checkout@v3 + + - name: Get short sha commit + id: git + run: | + echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT" + + - name: Get latest version + id: version + uses: ActionsTools/read-json-action@main + with: + file_path: "package.json" + + - name: Git + run: | + echo Short sha: ${{ steps.git.outputs.short_sha }} + echo Version is: ${{ steps.version.outputs.version }} + + - name: Environment + run: | + echo DOCKERFILE is: ${{ env.DOCKERFILE }} + echo DOCKERHUB_USERNAME is: ${{ env.DOCKERHUB_USERNAME }} + echo DOCKERHUB_TOKEN is: ${{ env.DOCKERHUB_TOKEN }} + echo DOCKERHUB_REPO_NAME is: ${{ env.DOCKERHUB_REPO_NAME }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx for Builder + uses: docker/setup-buildx-action@v3 + id: builder + + - name: Set up Docker Buildx for Main + uses: docker/setup-buildx-action@v3 + id: main + + - name: Builder name + run: echo ${{ steps.builder.outputs.name }} + + - name: Main name + run: echo ${{ steps.main.outputs.name }} + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_TOKEN }} + + - name: Build builder + uses: docker/build-push-action@v4 + with: + builder: ${{ steps.builder.outputs.name }} + file: ${{ env.DOCKERFILE }} + target: builder + + - name: Build main and push + uses: docker/build-push-action@v4 + if: ${{ github.ref_name == 'main' }} + with: + builder: ${{ steps.main.outputs.name }} + file: ${{ env.DOCKERFILE }} + build-args: | + NODE_ENV=production + target: main + tags: | + ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:latest + ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:main_v${{ steps.version.outputs.version }} + ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:main_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + push: true + + - name: Build staging and push + uses: docker/build-push-action@v4 + if: ${{ github.ref_name == 'staging' }} + with: + builder: ${{ steps.main.outputs.name }} + file: ${{ env.DOCKERFILE }} + build-args: | + NODE_ENV=staging + target: main + tags: | + ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:staging_v${{ steps.version.outputs.version }} + ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:staging_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + push: true + + deploy_production: + needs: [ build_image ] + runs-on: ubuntu-latest + if: ${{ github.ref_name == 'main' }} + environment: 'production' + + env: + DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKERHUB_REPO_NAME: ${{ vars.DOCKERHUB_REPO_NAME }} + + DOCKER_CONTAINER_NAME: ${{ vars.DOCKER_CONTAINER_NAME }} + DOCKER_CONTAINER_PORT: 3000 + DOCKER_CONTAINER_PORT_EXPOSE: ${{ vars.DOCKER_CONTAINER_PORT_EXPOSE }} + DOCKER_CONTAINER_NETWORK: app-network + + SSH_HOST: ${{ vars.SSH_HOST }} + SSH_PORT: ${{ vars.SSH_PORT }} + SSH_USER: ${{ vars.SSH_USER }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + + steps: + - name: Git checkout + uses: actions/checkout@v3 + + - name: Get short sha commit + id: git + run: | + echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT" + + - name: Get latest version + id: version + uses: ActionsTools/read-json-action@main + with: + file_path: "package.json" + + - name: Git + run: | + echo Short sha: ${{ steps.git.outputs.short_sha }} + echo Version is: ${{ steps.version.outputs.version }} + + - name: Environment + run: | + echo DOCKERHUB_USERNAME is: ${{ env.DOCKERHUB_USERNAME }} + echo DOCKERHUB_TOKEN is: ${{ env.DOCKERHUB_TOKEN }} + echo DOCKERHUB_REPO_NAME is: ${{ env.DOCKERHUB_REPO_NAME }} + echo DOCKER_CONTAINER_NAME is: ${{ env.DOCKER_CONTAINER_NAME }} + echo DOCKER_CONTAINER_PORT is: ${{ env.DOCKER_CONTAINER_PORT }} + echo DOCKER_CONTAINER_PORT_EXPOSE is: ${{ env.DOCKER_CONTAINER_PORT_EXPOSE }} + echo DOCKER_CONTAINER_NETWORK is: ${{ env.DOCKER_CONTAINER_NETWORK }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_TOKEN }} + + - name: Deploy + uses: fifsky/ssh-action@master + with: + command: | + docker pull ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:main_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + docker stop ${{ env.DOCKER_CONTAINER_NAME }} && docker rm ${{ env.DOCKER_CONTAINER_NAME }} + docker network create ${{ env.DOCKER_CONTAINER_NETWORK }} --driver=bridge + docker run -itd \ + --env NODE_ENV=production \ + --hostname ${{ env.DOCKER_CONTAINER_NAME }} \ + --publish ${{ env.DOCKER_CONTAINER_PORT_EXPOSE }}:${{ env.DOCKER_CONTAINER_PORT }} \ + --network ${{ env.DOCKER_CONTAINER_NETWORK }} \ + --volume /app/${{ env.DOCKER_CONTAINER_NAME }}/logs/:/app/logs/ \ + --volume /app/${{ env.DOCKER_CONTAINER_NAME }}/.env:/app/.env \ + --restart unless-stopped \ + --name ${{ env.DOCKER_CONTAINER_NAME }} ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:main_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + host: ${{ env.SSH_HOST }} + port: ${{ env.SSH_PORT }} + user: ${{ env.SSH_USER }} + key: ${{ env.SSH_PRIVATE_KEY }} + + - name: Clean + uses: fifsky/ssh-action@master + continue-on-error: true + with: + command: | + docker container prune --force + docker image prune --force + docker rmi $(docker images ${{ env.DOCKERHUB_USERNAME }}/** -q) --force + host: ${{ env.SSH_HOST }} + port: ${{ env.SSH_PORT }} + user: ${{ env.SSH_USER }} + key: ${{ env.SSH_PRIVATE_KEY }} + + deploy_staging: + needs: [ build_image ] + runs-on: ubuntu-latest + if: ${{ github.ref_name == 'staging' }} + environment: 'staging' + + env: + DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKERHUB_REPO_NAME: ${{ vars.DOCKERHUB_REPO_NAME }} + + DOCKER_CONTAINER_NAME: ${{ vars.DOCKER_CONTAINER_NAME }} + DOCKER_CONTAINER_PORT: 3000 + DOCKER_CONTAINER_PORT_EXPOSE: ${{ vars.DOCKER_CONTAINER_PORT_EXPOSE }} + DOCKER_CONTAINER_NETWORK: app-network + + SSH_HOST: ${{ vars.SSH_HOST }} + SSH_PORT: ${{ vars.SSH_PORT }} + SSH_USER: ${{ vars.SSH_USER }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + + steps: + - name: Git checkout + uses: actions/checkout@v3 + + - name: Get short sha commit + id: git + run: | + echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT" + + - name: Get latest version + id: version + uses: ActionsTools/read-json-action@main + with: + file_path: "package.json" + + - name: Git + run: | + echo Short sha: ${{ steps.git.outputs.short_sha }} + echo Version is: ${{ steps.version.outputs.version }} + + - name: Environment + run: | + echo DOCKERHUB_USERNAME is: ${{ env.DOCKERHUB_USERNAME }} + echo DOCKERHUB_TOKEN is: ${{ env.DOCKERHUB_TOKEN }} + echo DOCKERHUB_REPO_NAME is: ${{ env.DOCKERHUB_REPO_NAME }} + echo DOCKER_CONTAINER_NAME is: ${{ env.DOCKER_CONTAINER_NAME }} + echo DOCKER_CONTAINER_PORT is: ${{ env.DOCKER_CONTAINER_PORT }} + echo DOCKER_CONTAINER_PORT_EXPOSE is: ${{ env.DOCKER_CONTAINER_PORT_EXPOSE }} + echo DOCKER_CONTAINER_NETWORK is: ${{ env.DOCKER_CONTAINER_NETWORK }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_TOKEN }} + + - name: Deploy + uses: fifsky/ssh-action@master + with: + command: | + docker pull ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:staging_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + docker stop ${{ env.DOCKER_CONTAINER_NAME }} && docker rm ${{ env.DOCKER_CONTAINER_NAME }} + docker network create ${{ env.DOCKER_CONTAINER_NETWORK }} --driver=bridge + docker run -itd \ + --env NODE_ENV=development \ + --hostname ${{ env.DOCKER_CONTAINER_NAME }} \ + --publish ${{ env.DOCKER_CONTAINER_PORT_EXPOSE }}:${{ env.DOCKER_CONTAINER_PORT }} \ + --network ${{ env.DOCKER_CONTAINER_NETWORK }} \ + --volume /app/${{ env.DOCKER_CONTAINER_NAME }}/logs/:/app/logs/ \ + --volume /app/${{ env.DOCKER_CONTAINER_NAME }}/.env:/app/.env \ + --restart unless-stopped \ + --name ${{ env.DOCKER_CONTAINER_NAME }} ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPO_NAME }}:staging_v${{ steps.version.outputs.version }}_sha-${{ steps.git.outputs.short_sha }} + host: ${{ env.SSH_HOST }} + port: ${{ env.SSH_PORT }} + user: ${{ env.SSH_USER }} + key: ${{ env.SSH_PRIVATE_KEY }} + + - name: Clean + uses: fifsky/ssh-action@master + continue-on-error: true + with: + command: | + docker container prune --force + docker image prune --force + docker rmi $(docker images ${{ env.DOCKERHUB_USERNAME }}/** -q) --force + host: ${{ env.SSH_HOST }} + port: ${{ env.SSH_PORT }} + user: ${{ env.SSH_USER }} + key: ${{ env.SSH_PRIVATE_KEY }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index a0619f2aa..7e02f794f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,6 +5,8 @@ on: pull_request: branches: - main + - staging + - development jobs: linter: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2051cbd37..252305667 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,8 @@ on: pull_request: branches: - main + - staging + - development jobs: test: diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index 6f485d9a6..000000000 --- a/.lintstagedrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "{src,test}/**/*.{ts,tsx,json}": [ - "prettier --write '{src,test}/**/*.ts'", - "eslint", - "stop-only --file" - ] -} \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..d1cdf2f06 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict = true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 19786e1c8..80dc45467 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,31 +5,30 @@ "typescript", "typescriptreact" ], - "eslint.experimental.useFlatConfig": true, + "search.exclude": { + "package-lock.json": true + }, + + "editor.tabSize": 4, + "eslint.useFlatConfig": true, + "eslint.debug": true, + "eslint.options": { + "overrideConfigFile": "eslint.config.mjs" + }, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "typescript.preferences.importModuleSpecifier": "non-relative", - "[typescript]": { + "[typescript][json][jsonc][yaml]": { + "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "[dockercompose]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[yaml]": { - "editor.defaultFormatter": "redhat.vscode-yaml" - }, "[dockerfile]": { - "editor.defaultFormatter": "foxundermoon.shell-format" - }, - "[handlebars]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[dotenv]": { - "editor.defaultFormatter": "foxundermoon.shell-format" + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-azuretools.vscode-docker" } } diff --git a/README.md b/README.md index 3e76f5ffd..f13e0579e 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,29 @@ - [ACK NestJs Boilerplate 🔥 🚀](#ack-nestjs-boilerplate---) - [Table of contents](#table-of-contents) - [Important](#important) - - [Todo](#todo) + - [TODO](#todo) - [Prerequisites](#prerequisites) - [Build with](#build-with) - [Objective](#objective) - [Features](#features) - [Main Features](#main-features) - - [Database](#database) - - [Security](#security) - - [Setting](#setting) - - [Others](#others) - - [Third Party Integration](#third-party-integration) - [Installation](#installation) + - [Clone Repo](#clone-repo) + - [Install Dependencies](#install-dependencies) + - [Create environment](#create-environment) + - [Database Migration and Seed](#database-migration-and-seed) + - [Email Migration](#email-migration) + - [Run Project](#run-project) + - [Installation with Docker](#installation-with-docker) + - [Test](#test) - [Swagger](#swagger) + - [API Key](#api-key) + - [User](#user) + - [BullMQ Board](#bullmq-board) + - [User](#user-1) + - [Redis Client Web Base](#redis-client-web-base) + - [MongoDB Client Web Base](#mongodb-client-web-base) + - [User](#user-2) - [License](#license) - [Contribute](#contribute) - [Contact](#contact) @@ -59,16 +69,15 @@ 3. Global prefix will remove. Before is `/api`. * For monitoring, this project will use `sentry.io`, and only send `500` or `internal server error`. -## Todo +## TODO -* [x] Refactor to version 6, more straightforward -* [x] Add message remaining -* [ ] Refactor unit test -* [ ] Update Documentation, add behaviors -* [ ] Update Documentation, and include an diagram for easier comprehension -* [ ] Add Redis -* [ ] Move to stateful Authorization Token (security and ux reason) -* [ ] Implement GraphQL, just an options for running ? +- [ ] Export Module +- [ ] Move to Stateful Authorization + 1. Session Module + 2. Device Module + 3. Password Period Module + 5. Reset Password Module + 6. Verification Module ## Prerequisites @@ -121,66 +130,176 @@ Describes which version. * NestJs 10.x 🥳 * Typescript 🚀 * Production ready 🔥 +* MongoDB integrate by using [mongoose][ref-mongoose] 🎉 +* Cached response with redis +* Queue bullmq with redis +* Authorization, Role Management. * Repository Design Pattern (Multi Repository, can mix with other orm) -* Swagger / OpenAPI 3 included * Authentication (`Access Token`, `Refresh Token`, `API Key`, `Google SSO`, `Apple SSO`) -* Authorization, Role Management. +* Import and export data with CSV or Excel by using `decorator` * Support multi-language `i18n` 🗣, can controllable with request header `x-custom-lang` * Request validation for all request params, query, dan body with `class-validation` -* Serialization with `class-transformer` +* Swagger / OpenAPI 3 included * Url Versioning, default version is `1` * Server Side Pagination -* Import and export data with CSV or Excel by using `decorator` * Sentry.io for Monitoring Tools +* Support Docker installation +* Support CI/CD (Eg: Github Action) +* Husky GitHook for run linter before commit 🐶 +* Linter with EsLint for Typescript -### Database +## Installation -* MongoDB integrate by using [mongoose][ref-mongoose] 🎉 -* Multi Database -* Database Transaction -* Database Soft Delete -* Database Migration +Before start, we need to install some packages and tools. +The recommended version is the LTS version for every tool and package. +> Make sure to check that the tools have been installed successfully. -### Security +1. [NodeJs][ref-nodejs] +2. [MongoDB][ref-mongodb] +2. [Redis][ref-redis] +3. [Yarn][ref-yarn] +4. [Git][ref-git] -* Apply `helmet`, `cors`, and `throttler` -* Timeout awareness and can override ⌛️ -### Setting +### Clone Repo -* Support environment file -* Centralize configuration 🤖 -* Centralize response structure -* Centralize exception filter -* Setting from database 🗿 +Clone the project with git. -### Others +```bash +git clone https://github.com/andrechristikan/ack-nestjs-boilerplate.git +``` -* Support Docker installation -* Support CI/CD (Eg: Github Action) -* Husky GitHook for run linter before commit 🐶 -* Linter with EsLint for Typescript +### Install Dependencies +This project needs some dependencies. Let's go install it. -## Third Party Integration +```bash +yarn install +``` -* AWS S3 -* AWS SES -* AWS EC2 -* AWC ECS (ongoing) -* Sentry.io -* Google SSO -* Apple SSO +### Create environment -## Installation +Make your own environment file with a copy of `env.example` and adjust values to suit your own environment. + +```bash +cp .env.example .env +``` + +### Database Migration and Seed + +By default the options of `AutoCreate` and `AutoIndex` will be `false`. Thats means the schema in MongoDb will not change with the latest. +So to update the schema we need to run + +```bash +yarn migrate +``` + +After migrate the schema, also we need to run data seed + +```bash +yarn seed +``` + +### Email Migration + +> Optional + +The email will automatically create email template through AWS SES if we set the value at `.env` file + +For migrate +```bash +yarn migrate:email +``` + +### Run Project -Installation will describe in difference doc. [here][doc-installation]. +Finally, Cheers 🍻🍻 !!! you passed all steps. + +Now you can run the project. + +```bash +yarn start:dev +``` + +## Installation with Docker + +For docker installation, we need more tools to be installed. + +1. [Docker][ref-docker] +2. [Docker-Compose][ref-dockercompose] + +Make your own environment file with a copy of `env.example` and adjust values to suit your own environment. + +```bash +cp .env.example .env +``` + +then run + +```bash +docker-compose up -d +``` + +## Test + +The project only provide `unit testing`. + +```bash +yarn test +``` ## Swagger You can check The Swagger after running this project. Url `localhost:3000/docs` and don't for get to put `x-api-key` on header. + +### API Key + +api key: `v8VB0yY887lMpTA2VJMV` +api key secret: `zeZbtGTugBTn3Qd5UXtSZBwt7gn3bg` + +### User + +1. Super Admin + - email: `superadmin@mail.com` + - password: `aaAA@123` +2. Admin + - email: `admin@mail.com` + - password: `aaAA@123` +3. Member + - email: `member@mail.com` + - password: `aaAA@123` +4. User + - email: `user@mail.com` + - password: `aaAA@123` + +## BullMQ Board + +> This available with docker installation + +You can check and monitor your queue. Url `localhost:3010` + +### User + - email: `admin` + - password: `admin123` + +## Redis Client Web Base + +> This available with docker installation + +You can check redis data using `redis-commander`. Url `localhost:3011` + +## MongoDB Client Web Base + +> This available with docker installation + +You can check mongodb data using `mongo-express`. Url `localhost:3012` + +### User + - email: `admin` + - password: `admin123` + ## License Distributed under [MIT licensed][license]. @@ -190,12 +309,12 @@ Distributed under [MIT licensed][license]. How to contribute in this repo 1. Fork the repository -2. Create your branch (git checkout -b my-branch) +2. Create your branch `git checkout -b my-branch` 3. Commit any changes to your branch 4. Push your changes to your remote branch 5. Open a pull request -If your code behind commit with the original / main / master branch, please update your code and resolve the conflict. +If your code behind commit with the `original/main branch`, please update your code and resolve the conflict. ## Contact @@ -237,9 +356,6 @@ If your code behind commit with the original / main / master branch, please upda [license]: LICENSE.md - -[doc-installation]: /docs/installation.md - [ref-nestjs]: http://nestjs.com [ref-mongoose]: https://mongoosejs.com @@ -254,4 +370,4 @@ If your code behind commit with the original / main / master branch, please upda [ref-jwt]: https://jwt.io [ref-jest]: https://jestjs.io/docs/getting-started [ref-git]: https://git-scm.com - +[ref-redis]: https://redis.io diff --git a/ci/dockerfile b/ci/dockerfile new file mode 100644 index 000000000..6ce85b7f1 --- /dev/null +++ b/ci/dockerfile @@ -0,0 +1,31 @@ +# Test Image +FROM node:lts-alpine as builder +LABEL maintainer "andrechristikan@mail.com" + +ENV NODE_ENV=test + +WORKDIR /app +COPY package.json yarn.lock ./ + +RUN set -x && yarn --frozen-lockfile --non-interactive + +COPY . . + +RUN yarn build + +# Production Image +FROM node:lts-alpine as main +LABEL maintainer "andrechristikan@mail.com" + +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /app +EXPOSE 3000 + +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/dist ./dist +RUN touch .env + +RUN set -x && yarn --frozen-lockfile --non-interactive --production + +CMD ["yarn", "start:prod"] diff --git a/cspell.json b/cspell.json index e60be7b93..7bff1ee3d 100644 --- a/cspell.json +++ b/cspell.json @@ -29,10 +29,23 @@ "Streamable", "superadmin", "userhistories", - "userpasswords" + "userpasswords", + "VJMV", + "golevelup", + "abcdefghijkl", + "ijkl", + "ssword", + "safestring", + "testuser", + "emiting", + "presignUrl", + "presign", + "presigner", + "presigned", + "bullmq" ], "ignorePaths": [ - "node_modules/**", + "node_modules/**", "endpoints/**", "*coverage/**", ".husky/**", diff --git a/docker-compose.yml b/docker-compose.yml index 43169741f..a6a155d52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,158 @@ + version: '3.8' +name: ack + services: - boilerack: + service: build: context: . args: NODE_ENV: 'development' - container_name: boilerack - hostname: boilerack + container_name: service + hostname: service ports: - 3000:3000 volumes: - ./src/:/app/src/ - .env/:/app/.env restart: always + networks: + - app-network + + redis: + image: redis:latest + container_name: redis + hostname: redis + restart: always + ports: + - '6379:6379' + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 30s + retries: 5 + + redis-bullboard: + image: deadly0/bull-board:latest + container_name: redis-bullboard + hostname: redis-bullboard + restart: always + ports: + - 3010:3000 + networks: + - app-network + environment: + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_DB=0 + - USER_LOGIN=admin + - USER_PASSWORD=admin123 + depends_on: + - redis + + redis-commander: + image: ghcr.io/joeferner/redis-commander:latest + container_name: redis-commander + hostname: redis-commander + restart: always + ports: + - 3011:8081 + networks: + - app-network + environment: + - REDIS_HOSTS=local:redis:6379 + depends_on: + - redis + + mongo-express: + image: mongo-express:latest + container_name: mongo-express + hostname: mongo-express + environment: + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin123 + ME_CONFIG_MONGODB_URL: mongodb://host.docker.internal:27017,host.docker.internal:27018,host.docker.internal:27019/ack?retryWrites=true&w=majority&replicaSet=rs0 + ports: + - 3012:8081 + networks: + - app-network + depends_on: + - mongodb1 + - mongodb2 + - mongodb3 + + mongodb1: + container_name: mongodb1 + hostname: mongodb1 + image: mongo:latest + restart: always + ports: + - "27017:27017" + links: + - mongodb2 + - mongodb3 + depends_on: + mongodb2: + condition: service_started + mongodb3: + condition: service_started + networks: + - app-network + volumes: + - mongodb1_data:/data/db + - mongodb1_config:/data/configdb + command: mongod --bind_ip_all --replSet rs0 --port 27017 + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'host.docker.internal:27017',priority:1},{_id:1,host:'host.docker.internal:27018',priority:0.5},{_id:2,host:'host.docker.internal:27019',priority:0.5}]}) }" | mongosh --port 27017 --quiet + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 5 + + mongodb2: + container_name: mongodb2 + hostname: mongodb2 + image: mongo:latest + networks: + - app-network + restart: always + ports: + - "27018:27018" + volumes: + - mongodb2_data:/data/db + - mongodb2_config:/data/configdb + command: mongod --bind_ip_all --replSet rs0 --port 27018 + + mongodb3: + container_name: mongodb3 + hostname: mongodb3 + image: mongo:latest + networks: + - app-network + restart: always + ports: + - "27019:27019" + volumes: + - mongodb3_data:/data/db + - mongodb3_config:/data/configdb + command: mongod --bind_ip_all --replSet rs0 --port 27019 + +volumes: + mongodb1_data: + mongodb2_data: + mongodb3_data: + mongodb1_config: + mongodb2_config: + mongodb3_config: + redis_data: +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/dockerfile b/dockerfile index 09b538fd5..59c7564e8 100644 --- a/dockerfile +++ b/dockerfile @@ -9,7 +9,6 @@ EXPOSE 3000 COPY package.json yarn.lock ./ RUN touch .env -RUN mkdir data RUN set -x && yarn --frozen-lockfile COPY . . diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 86f804669..000000000 --- a/docs/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Installation - -## Getting Started - -Before start, we need to install some packages and tools. -The recommended version is the LTS version for every tool and package. - -> Make sure to check that the tools have been installed successfully. - -1. [NodeJs][ref-nodejs] -2. [MongoDB][ref-mongodb] -3. [Yarn][ref-yarn] -4. [Git][ref-git] - -### Clone Repo - -Clone the project with git. - -```bash -git clone https://github.com/andrechristikan/ack-nestjs-boilerplate.git -``` - -### Install Dependencies - -This project needs some dependencies. Let's go install it. - -```bash -yarn install -``` - -### Create environment - -Make your own environment file with a copy of `env.example` and adjust values to suit your own environment. - -```bash -cp .env.example .env -``` - -### Test - -> Next development will add e2e test - -The project only provide `unit testing`. - -```bash -yarn test -``` - -## Run Project - -Finally, Cheers 🍻🍻 !!! you passed all steps. - -Now you can run the project. - -```bash -yarn start:dev -``` - -## Run Project with Docker - -For docker installation, we need more tools to be installed. - -1. [Docker][ref-docker] -2. [Docker-Compose][ref-dockercompose] - -### Create environment - -Make your own environment file with a copy of `env.example` and adjust values to suit your own environment. - -```bash -cp .env.example .env -``` - -then run - -```bash -docker-compose up -d -``` - -## Database Migration - -This project need to do migration for running. [Read this][ack-database-migration-doc] - -## Email Migration - -The email will automatically create email template through AWS SES - -For migrate -```bash -yarn migrate:email -``` - - - -[ack-database-migration-doc]: /docs/database_migration.md - - -[ref-mongodb]: https://docs.mongodb.com/ -[ref-nodejs]: https://nodejs.org/ -[ref-docker]: https://docs.docker.com -[ref-dockercompose]: https://docs.docker.com/compose/ -[ref-git]: https://git-scm.com diff --git a/docs/database_migration.md b/docs/database_migration.md deleted file mode 100644 index 887a39cf4..000000000 --- a/docs/database_migration.md +++ /dev/null @@ -1,40 +0,0 @@ - -# Database Migration - -> The migration will do data seeding to MongoDB. Make sure to check the value of the `DATABASE_` prefix in your`.env` file. - -The Database migration used [NestJs-Command][ref-nestjscommand] - -For seeding - -```bash -yarn seed -``` - -For remove all data do - -```bash -yarn rollback -``` - -# API Key Test -api key: `v8VB0yY887lMpTA2VJMV` -api key secret: `zeZbtGTugBTn3Qd5UXtSZBwt7gn3bg` - -# User Test - -1. Super Admin - - email: `superadmin@mail.com` - - password: `aaAA@123` -2. Admin - - email: `admin@mail.com` - - password: `aaAA@123` -3. Member - - email: `member@mail.com` - - password: `aaAA@123` -4. User - - email: `user@mail.com` - - password: `aaAA@123` - - -[ref-nestjscommand]: https://gitlab.com/aa900031/nestjs-command diff --git a/eslint.config.mjs b/eslint.config.mjs index 8b241e5ed..daf660973 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,16 +15,14 @@ export default [ '.github/*', '.husky/*', 'coverage/*', - 'data/*', 'dist/*', 'docs/*', - 'logs/*', 'node_modules/*', ], }, { name: 'ts/default', - files: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}'], + files: ['src/**/*.ts'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -46,4 +44,28 @@ export default [ '@typescript-eslint/no-explicit-any': 'off', }, }, + { + name: 'ts/test', + files: ['test/**/*.ts'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parser: tsParser, + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: '.', + }, + }, + linterOptions: { + noInlineConfig: false, + reportUnusedDisableDirectives: true, + }, + plugins: { + '@typescript-eslint': tsEsLintPlugin, + }, + rules: { + ...rules, + '@typescript-eslint/no-explicit-any': 'off', + }, + }, ]; diff --git a/nest-cli.json b/nest-cli.json index 415147d8e..d40c3532b 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,18 +1,16 @@ { - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "plugins": [ - "@nestjs/swagger" - ], - "assets": [ - { - "include": "languages/**/*", - "outDir": "dist/src" - } - ], - "webpack": false, - "deleteOutDir": true, - "watchAssets": true - } -} \ No newline at end of file + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "plugins": ["@nestjs/swagger"], + "assets": [ + { + "include": "languages/**/*", + "outDir": "dist/src" + } + ], + "webpack": false, + "deleteOutDir": true, + "watchAssets": true + } +} diff --git a/package.json b/package.json index a5eafa4da..853d907ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ack-nestjs-boilerplate", - "version": "6.1.1", + "version": "7.0.0", "description": "Ack NestJs Boilerplate", "repository": { "type": "git", @@ -27,40 +27,47 @@ "deadcode": "ts-prune --project tsconfig.json --unusedInModule --skip *.json", "spell": "cspell lint --config cspell.json '{src,test}/**/*.ts' --color --gitignore --no-must-find-files --no-summary --no-progress || true", "upgrade:package": "ncu -u", + "migrate": "APP_ENV=migration nest start", "migrate:email": "nestjs-command migrate:email", + "rollback:email": "nestjs-command rollback:email", "seed": "nestjs-command seed:country && nestjs-command seed:apikey && nestjs-command seed:role && nestjs-command seed:user", "rollback": "nestjs-command remove:country && nestjs-command remove:apikey && nestjs-command remove:user && nestjs-command remove:role" }, "dependencies": { - "@aws-sdk/client-s3": "^3.598.0", - "@aws-sdk/client-ses": "^3.598.0", + "@aws-sdk/client-s3": "^3.629.0", + "@aws-sdk/client-ses": "^3.629.0", + "@aws-sdk/s3-request-presigner": "^3.629.0", "@casl/ability": "^6.7.1", "@faker-js/faker": "^8.4.1", "@nestjs/axios": "^3.0.2", - "@nestjs/common": "^10.3.9", - "@nestjs/config": "^3.2.2", - "@nestjs/core": "^10.3.9", + "@nestjs/bullmq": "^10.2.0", + "@nestjs/cache-manager": "^2.2.2", + "@nestjs/common": "^10.4.0", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.0", "@nestjs/jwt": "^10.2.0", - "@nestjs/mongoose": "^10.0.6", + "@nestjs/mongoose": "^10.0.10", "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.9", - "@nestjs/schedule": "^4.0.2", - "@nestjs/swagger": "^7.3.1", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/swagger": "^7.4.0", "@nestjs/terminus": "^10.2.3", - "@nestjs/throttler": "^5.2.0", + "@nestjs/throttler": "^6.1.0", "@ntegral/nestjs-sentry": "^4.0.1", - "@sentry/node": "^8.9.2", - "axios": "^1.7.2", + "@sentry/node": "^8.25.0", + "axios": "^1.7.3", "bcryptjs": "^2.4.3", + "bullmq": "^5.12.5", + "cache-manager": "^5.7.6", + "cache-manager-redis-store": "^3.0.1", "case": "^1.6.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "crypto-js": "^4.2.0", - "google-auth-library": "^9.11.0", + "google-auth-library": "^9.13.0", "helmet": "^7.1.0", "moment": "^2.30.1", "moment-timezone": "^0.5.45", - "mongoose": "^8.4.1", + "mongoose": "^8.5.2", "nestjs-command": "^3.1.4", "nestjs-i18n": "^10.4.5", "passport": "^0.7.0", @@ -75,43 +82,60 @@ "yarn": "^1.22.22" }, "devDependencies": { - "@commitlint/cli": "^19.3.0", + "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", - "@eslint/js": "^9.5.0", - "@nestjs/cli": "^10.3.2", - "@nestjs/schematics": "^10.1.1", - "@nestjs/testing": "^10.3.9", + "@eslint/js": "^9.9.0", + "@golevelup/ts-jest": "^0.5.2", + "@nestjs/cli": "^10.4.4", + "@nestjs/schematics": "^10.1.3", + "@nestjs/testing": "^10.4.0", "@types/bcryptjs": "^2.4.6", "@types/bytes": "^3.1.4", "@types/cors": "^2.8.17", - "@types/cron": "^2.0.1", "@types/crypto-js": "^4.2.2", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", "@types/jest": "^29.5.12", - "@types/lodash": "^4.17.5", + "@types/lodash": "^4.17.7", "@types/ms": "^0.7.34", "@types/multer": "^1.4.11", - "@types/node": "^20.14.2", + "@types/node": "^22.2.0", "@types/passport-jwt": "^4.0.1", "@types/response-time": "^2.3.8", "@types/supertest": "^6.0.2", - "@types/uuid": "^9.0.8", - "cspell": "^8.8.4", - "eslint": "^9.5.0", + "@types/uuid": "^10.0.0", + "cspell": "^8.13.3", + "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", - "husky": "^9.0.11", + "husky": "^9.1.4", "jest": "^29.7.0", - "lint-staged": "^15.2.7", - "prettier": "^3.3.2", - "stop-only": "^3.3.2", + "lint-staged": "^15.2.9", + "prettier": "^3.3.3", + "rxjs-marbles": "^7.0.1", + "stop-only": "^3.3.3", "supertest": "^7.0.0", - "ts-jest": "^29.1.5", + "ts-jest": "^29.2.4", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "ts-prune": "^0.10.3", "tsconfig-paths": "^4.2.0", - "typescript": "^5.4.5", - "typescript-eslint": "^7.13.0" + "typescript": "^5.5.4", + "typescript-eslint": "^8.1.0" + }, + "lint-staged": { + "**/*.ts": [ + "prettier --write '{src,test}/**/*.ts'", + "eslint", + "stop-only --file" + ], + "*.{json,yaml}": [ + "prettier --write" + ] + }, + "packageManager": "yarn@1.22.21", + "engines": { + "npm": "please-use-yarn", + "yarn": ">= 1.22.21", + "node": ">= 20.11.0" } } diff --git a/src/app/app.middleware.module.ts b/src/app/app.middleware.module.ts index 5cd176bf7..febb5726d 100644 --- a/src/app/app.middleware.module.ts +++ b/src/app/app.middleware.module.ts @@ -12,22 +12,22 @@ import { ThrottlerModuleOptions, } from '@nestjs/throttler'; import { SentryModule } from '@ntegral/nestjs-sentry'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; import { AppGeneralFilter } from 'src/app/filters/app.general.filter'; import { AppHttpFilter } from 'src/app/filters/app.http.filter'; import { AppValidationImportFilter } from 'src/app/filters/app.validation-import.filter'; import { AppValidationFilter } from 'src/app/filters/app.validation.filter'; import { - JsonBodyParserMiddleware, - RawBodyParserMiddleware, - TextBodyParserMiddleware, - UrlencodedBodyParserMiddleware, -} from 'src/app/middlewares/body-parser.middleware'; -import { CorsMiddleware } from 'src/app/middlewares/cors.middleware'; -import { MessageCustomLanguageMiddleware } from 'src/app/middlewares/custom-language.middleware'; -import { HelmetMiddleware } from 'src/app/middlewares/helmet.middleware'; -import { ResponseTimeMiddleware } from 'src/app/middlewares/response-time.middleware'; -import { UrlVersionMiddleware } from 'src/app/middlewares/url-version.middleware'; + AppJsonBodyParserMiddleware, + AppRawBodyParserMiddleware, + AppTextBodyParserMiddleware, + AppUrlencodedBodyParserMiddleware, +} from 'src/app/middlewares/app.body-parser.middleware'; +import { AppCorsMiddleware } from 'src/app/middlewares/app.cors.middleware'; +import { AppCustomLanguageMiddleware } from 'src/app/middlewares/app.custom-language.middleware'; +import { AppHelmetMiddleware } from 'src/app/middlewares/app.helmet.middleware'; +import { AppResponseTimeMiddleware } from 'src/app/middlewares/app.response-time.middleware'; +import { AppUrlVersionMiddleware } from 'src/app/middlewares/app.url-version.middleware'; @Module({ controllers: [], @@ -68,6 +68,7 @@ import { UrlVersionMiddleware } from 'src/app/middlewares/url-version.middleware }), }), SentryModule.forRootAsync({ + inject: [ConfigService], imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ dsn: configService.get('debug.sentry.dsn'), @@ -82,7 +83,6 @@ import { UrlVersionMiddleware } from 'src/app/middlewares/url-version.middleware timeout: configService.get('debug.sentry.timeout'), }, }), - inject: [ConfigService], }), ], }) @@ -90,15 +90,15 @@ export class AppMiddlewareModule implements NestModule { configure(consumer: MiddlewareConsumer): void { consumer .apply( - HelmetMiddleware, - JsonBodyParserMiddleware, - TextBodyParserMiddleware, - RawBodyParserMiddleware, - UrlencodedBodyParserMiddleware, - CorsMiddleware, - UrlVersionMiddleware, - ResponseTimeMiddleware, - MessageCustomLanguageMiddleware + AppHelmetMiddleware, + AppJsonBodyParserMiddleware, + AppTextBodyParserMiddleware, + AppRawBodyParserMiddleware, + AppUrlencodedBodyParserMiddleware, + AppCorsMiddleware, + AppUrlVersionMiddleware, + AppResponseTimeMiddleware, + AppCustomLanguageMiddleware ) .forRoutes('*'); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7de05fbf6..9ba36b799 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,22 +1,22 @@ import { Module } from '@nestjs/common'; -import { JobsModule } from 'src/jobs/jobs.module'; import { RouterModule } from 'src/router/router.module'; import { CommonModule } from 'src/common/common.module'; import { AppMiddlewareModule } from 'src/app/app.middleware.module'; +import { WorkerModule } from 'src/worker/worker.module'; @Module({ controllers: [], providers: [], imports: [ // Common - AppMiddlewareModule, CommonModule, - - // Jobs - JobsModule.forRoot(), + AppMiddlewareModule, // Routes RouterModule.forRoot(), + + // Workers + WorkerModule, ], }) export class AppModule {} diff --git a/src/app/dtos/app.env.dto.ts b/src/app/dtos/app.env.dto.ts index 91949c000..5ce8b2e81 100644 --- a/src/app/dtos/app.env.dto.ts +++ b/src/app/dtos/app.env.dto.ts @@ -6,12 +6,13 @@ import { IsNumber, IsOptional, IsString, + IsUrl, } from 'class-validator'; import { ENUM_APP_ENVIRONMENT, ENUM_APP_TIMEZONE, -} from 'src/app/constants/app.enum.constant'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +} from 'src/app/enums/app.enum'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; export class AppEnvDto { @IsString() @@ -62,11 +63,6 @@ export class AppEnvDto { @Type(() => Number) URL_VERSION: number; - @IsBoolean() - @IsNotEmpty() - @Type(() => Boolean) - JOB_ENABLE: boolean; - @IsNotEmpty() @IsString() DATABASE_URI: string; @@ -76,10 +72,6 @@ export class AppEnvDto { @Type(() => Boolean) DATABASE_DEBUG: boolean; - @IsNotEmpty() - @IsString() - AUTH_JWT_SUBJECT: string; - @IsNotEmpty() @IsString() AUTH_JWT_AUDIENCE: string; @@ -148,7 +140,30 @@ export class AppEnvDto { @IsString() AUTH_SOCIAL_APPLE_SIGN_IN_CLIENT_ID?: string; + @IsNotEmpty() + @IsString() + REDIS_HOST: string; + + @IsNumber() + @IsNotEmpty() + @Type(() => Number) + REDIS_PORT: number; + + @IsOptional() + @IsString() + REDIS_PASSWORD?: string; + + @IsNotEmpty() + @IsBoolean() + @Type(() => Boolean) + REDIS_TLS: boolean; + @IsOptional() @IsString() SENTRY_DSN?: string; + + @IsNotEmpty() + @IsUrl() + @IsString() + CLIENT_URL: string; } diff --git a/src/app/constants/app.enum.constant.ts b/src/app/enums/app.enum.ts similarity index 83% rename from src/app/constants/app.enum.constant.ts rename to src/app/enums/app.enum.ts index 2df123aa7..9f8844a33 100644 --- a/src/app/constants/app.enum.constant.ts +++ b/src/app/enums/app.enum.ts @@ -1,10 +1,10 @@ export enum ENUM_APP_ENVIRONMENT { PRODUCTION = 'production', + MIGRATION = 'migration', STAGING = 'staging', DEVELOPMENT = 'development', } export enum ENUM_APP_TIMEZONE { - ASIA_SINGAPORE = 'Asia/Singapore', ASIA_JAKARTA = 'Asia/Jakarta', } diff --git a/src/app/constants/app.status-code.constant.ts b/src/app/enums/app.status-code.enum.ts similarity index 62% rename from src/app/constants/app.status-code.constant.ts rename to src/app/enums/app.status-code.enum.ts index e69650f9f..3fef07f16 100644 --- a/src/app/constants/app.status-code.constant.ts +++ b/src/app/enums/app.status-code.enum.ts @@ -1,3 +1,3 @@ export enum ENUM_APP_STATUS_CODE_ERROR { - UNKNOWN_ERROR = 5040, + UNKNOWN = 5040, } diff --git a/src/app/filters/app.general.filter.ts b/src/app/filters/app.general.filter.ts index fabbc61dd..059a75455 100644 --- a/src/app/filters/app.general.filter.ts +++ b/src/app/filters/app.general.filter.ts @@ -117,7 +117,11 @@ export class AppGeneralFilter implements ExceptionFilter { try { this.sentryService.instance().captureException(exception); - } catch (err: unknown) {} + } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + } return; } diff --git a/src/app/filters/app.http.filter.ts b/src/app/filters/app.http.filter.ts index 7d45b3f8e..12914c800 100644 --- a/src/app/filters/app.http.filter.ts +++ b/src/app/filters/app.http.filter.ts @@ -11,10 +11,7 @@ import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; import { IAppException } from 'src/app/interfaces/app.interface'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { - IMessageOptionsProperties, - IMessageValidationError, -} from 'src/common/message/interfaces/message.interface'; +import { IMessageOptionsProperties } from 'src/common/message/interfaces/message.interface'; import { MessageService } from 'src/common/message/services/message.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; import { ResponseMetadataDto } from 'src/common/response/dtos/response.dto'; @@ -58,9 +55,8 @@ export class AppHttpFilter implements ExceptionFilter { let statusHttp: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR; let messagePath = `http.${statusHttp}`; let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; - const errors: IMessageValidationError[] = undefined; - let messageProperties: IMessageOptionsProperties = undefined; - let data: Record = undefined; + let messageProperties: IMessageOptionsProperties; + let data: Record; // metadata const xLanguage: string = @@ -109,7 +105,6 @@ export class AppHttpFilter implements ExceptionFilter { const responseBody: IAppException = { statusCode, message, - errors, _metadata: metadata, data, }; diff --git a/src/app/filters/app.validation-import.filter.ts b/src/app/filters/app.validation-import.filter.ts index f42e3d366..f098f83f9 100644 --- a/src/app/filters/app.validation-import.filter.ts +++ b/src/app/filters/app.validation-import.filter.ts @@ -1,10 +1,4 @@ -import { - ExceptionFilter, - Catch, - ArgumentsHost, - HttpStatus, - Logger, -} from '@nestjs/common'; +import { ExceptionFilter, Catch, ArgumentsHost, Logger } from '@nestjs/common'; import { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; @@ -44,12 +38,6 @@ export class AppValidationImportFilter implements ExceptionFilter { this.logger.error(exception); } - // set default - const responseException = - exception.getResponse() as IAppImportException; - const statusHttp: HttpStatus = exception.getStatus(); - const statusCode = responseException.statusCode; - // metadata const xLanguage: string = request.__language ?? this.messageService.getLanguage(); @@ -69,22 +57,19 @@ export class AppValidationImportFilter implements ExceptionFilter { }; // set response - const message = this.messageService.setMessage( - responseException.message, - { - customLanguage: xLanguage, - } - ); + const message = this.messageService.setMessage(exception.message, { + customLanguage: xLanguage, + }); const errors: IMessageValidationImportError[] = this.messageService.setValidationImportMessage( - responseException.errors as IMessageValidationImportErrorParam[], + exception.errors as IMessageValidationImportErrorParam[], { customLanguage: xLanguage, } ); const responseBody: IAppImportException = { - statusCode, + statusCode: exception.statusCode, message, errors, _metadata: metadata, @@ -96,7 +81,7 @@ export class AppValidationImportFilter implements ExceptionFilter { .setHeader('x-timezone', xTimezone) .setHeader('x-version', xVersion) .setHeader('x-repo-version', xRepoVersion) - .status(statusHttp) + .status(exception.httpStatus) .json(responseBody); return; diff --git a/src/app/filters/app.validation.filter.ts b/src/app/filters/app.validation.filter.ts index 45200bbc2..9275f462d 100644 --- a/src/app/filters/app.validation.filter.ts +++ b/src/app/filters/app.validation.filter.ts @@ -1,10 +1,4 @@ -import { - ExceptionFilter, - Catch, - ArgumentsHost, - HttpStatus, - Logger, -} from '@nestjs/common'; +import { ExceptionFilter, Catch, ArgumentsHost, Logger } from '@nestjs/common'; import { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; @@ -41,11 +35,6 @@ export class AppValidationFilter implements ExceptionFilter { this.logger.error(exception); } - // set default - const responseException = exception.getResponse() as IAppException; - const statusHttp: HttpStatus = exception.getStatus(); - const statusCode = responseException.statusCode; - // metadata const xLanguage: string = request.__language ?? this.messageService.getLanguage(); @@ -69,12 +58,12 @@ export class AppValidationFilter implements ExceptionFilter { customLanguage: xLanguage, }); const errors: IMessageValidationError[] = - this.messageService.setValidationMessage(responseException.errors, { + this.messageService.setValidationMessage(exception.errors, { customLanguage: xLanguage, }); const responseBody: IAppException = { - statusCode, + statusCode: exception.statusCode, message, errors, _metadata: metadata, @@ -86,7 +75,7 @@ export class AppValidationFilter implements ExceptionFilter { .setHeader('x-timezone', xTimezone) .setHeader('x-version', xVersion) .setHeader('x-repo-version', xRepoVersion) - .status(statusHttp) + .status(exception.httpStatus) .json(responseBody); return; diff --git a/src/app/middlewares/body-parser.middleware.ts b/src/app/middlewares/app.body-parser.middleware.ts similarity index 86% rename from src/app/middlewares/body-parser.middleware.ts rename to src/app/middlewares/app.body-parser.middleware.ts index 4ecb8d9dd..beed67b4d 100644 --- a/src/app/middlewares/body-parser.middleware.ts +++ b/src/app/middlewares/app.body-parser.middleware.ts @@ -4,7 +4,7 @@ import bodyParser from 'body-parser'; import { ConfigService } from '@nestjs/config'; @Injectable() -export class UrlencodedBodyParserMiddleware implements NestMiddleware { +export class AppUrlencodedBodyParserMiddleware implements NestMiddleware { private readonly maxFile: number; constructor(private readonly configService: ConfigService) { @@ -22,7 +22,7 @@ export class UrlencodedBodyParserMiddleware implements NestMiddleware { } @Injectable() -export class JsonBodyParserMiddleware implements NestMiddleware { +export class AppJsonBodyParserMiddleware implements NestMiddleware { private readonly maxFile: number; constructor(private readonly configService: ConfigService) { @@ -39,7 +39,7 @@ export class JsonBodyParserMiddleware implements NestMiddleware { } @Injectable() -export class RawBodyParserMiddleware implements NestMiddleware { +export class AppRawBodyParserMiddleware implements NestMiddleware { private readonly maxFile: number; constructor(private readonly configService: ConfigService) { @@ -56,7 +56,7 @@ export class RawBodyParserMiddleware implements NestMiddleware { } @Injectable() -export class TextBodyParserMiddleware implements NestMiddleware { +export class AppTextBodyParserMiddleware implements NestMiddleware { private readonly maxFile: number; constructor(private readonly configService: ConfigService) { diff --git a/src/app/middlewares/cors.middleware.ts b/src/app/middlewares/app.cors.middleware.ts similarity index 91% rename from src/app/middlewares/cors.middleware.ts rename to src/app/middlewares/app.cors.middleware.ts index af66ce83d..60cee28a4 100644 --- a/src/app/middlewares/cors.middleware.ts +++ b/src/app/middlewares/app.cors.middleware.ts @@ -2,10 +2,10 @@ import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import cors, { CorsOptions } from 'cors'; import { ConfigService } from '@nestjs/config'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; @Injectable() -export class CorsMiddleware implements NestMiddleware { +export class AppCorsMiddleware implements NestMiddleware { private readonly appEnv: ENUM_APP_ENVIRONMENT; private readonly allowOrigin: string | boolean | string[]; private readonly allowMethod: string[]; diff --git a/src/app/middlewares/custom-language.middleware.ts b/src/app/middlewares/app.custom-language.middleware.ts similarity index 95% rename from src/app/middlewares/custom-language.middleware.ts rename to src/app/middlewares/app.custom-language.middleware.ts index 7cffd8fd3..a38794839 100644 --- a/src/app/middlewares/custom-language.middleware.ts +++ b/src/app/middlewares/app.custom-language.middleware.ts @@ -5,7 +5,7 @@ import { HelperArrayService } from 'src/common/helper/services/helper.array.serv import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @Injectable() -export class MessageCustomLanguageMiddleware implements NestMiddleware { +export class AppCustomLanguageMiddleware implements NestMiddleware { private readonly availableLanguage: string[]; constructor( diff --git a/src/app/middlewares/helmet.middleware.ts b/src/app/middlewares/app.helmet.middleware.ts similarity index 81% rename from src/app/middlewares/helmet.middleware.ts rename to src/app/middlewares/app.helmet.middleware.ts index 67935eef6..6e7108d66 100644 --- a/src/app/middlewares/helmet.middleware.ts +++ b/src/app/middlewares/app.helmet.middleware.ts @@ -3,7 +3,7 @@ import { Request, Response, NextFunction } from 'express'; import helmet from 'helmet'; @Injectable() -export class HelmetMiddleware implements NestMiddleware { +export class AppHelmetMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction): void { helmet()(req, res, next); } diff --git a/src/app/middlewares/response-time.middleware.ts b/src/app/middlewares/app.response-time.middleware.ts similarity index 81% rename from src/app/middlewares/response-time.middleware.ts rename to src/app/middlewares/app.response-time.middleware.ts index 185f579a5..a0ccc782b 100644 --- a/src/app/middlewares/response-time.middleware.ts +++ b/src/app/middlewares/app.response-time.middleware.ts @@ -3,7 +3,7 @@ import { Request, Response, NextFunction } from 'express'; import responseTime from 'response-time'; @Injectable() -export class ResponseTimeMiddleware implements NestMiddleware { +export class AppResponseTimeMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction): Promise { responseTime()(req, res, next); } diff --git a/src/app/middlewares/url-version.middleware.ts b/src/app/middlewares/app.url-version.middleware.ts similarity index 92% rename from src/app/middlewares/url-version.middleware.ts rename to src/app/middlewares/app.url-version.middleware.ts index 345982986..2a90e1f33 100644 --- a/src/app/middlewares/url-version.middleware.ts +++ b/src/app/middlewares/app.url-version.middleware.ts @@ -1,11 +1,11 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Response, NextFunction } from 'express'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @Injectable() -export class UrlVersionMiddleware implements NestMiddleware { +export class AppUrlVersionMiddleware implements NestMiddleware { private readonly env: ENUM_APP_ENVIRONMENT; private readonly globalPrefix: string; diff --git a/src/cli.ts b/src/cli.ts index 3140fe849..ccb49c0b1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { CommandModule, CommandService } from 'nestjs-command'; import { MigrationModule } from 'src/migration/migration.module'; @@ -7,10 +8,14 @@ async function bootstrap() { logger: ['error'], }); + const logger = new Logger('NestJs-Seed'); + try { await app.select(CommandModule).get(CommandService).exec(); process.exit(0); } catch (err: unknown) { + logger.error(err); + process.exit(1); } } diff --git a/src/common/api-key/constants/api-key.enum.constant.ts b/src/common/api-key/constants/api-key.enum.constant.ts deleted file mode 100644 index cddc36518..000000000 --- a/src/common/api-key/constants/api-key.enum.constant.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ENUM_API_KEY_TYPE { - PRIVATE = 'PRIVATE', - PUBLIC = 'PUBLIC', -} diff --git a/src/common/api-key/constants/api-key.status-code.constant.ts b/src/common/api-key/constants/api-key.status-code.constant.ts deleted file mode 100644 index 0a966f5e2..000000000 --- a/src/common/api-key/constants/api-key.status-code.constant.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum ENUM_API_KEY_STATUS_CODE_ERROR { - X_API_KEY_REQUIRED_ERROR = 5050, - X_API_KEY_NOT_FOUND_ERROR = 5051, - X_API_KEY_INACTIVE_ERROR = 5052, - X_API_KEY_EXPIRED_ERROR = 5053, - X_API_KEY_INVALID_ERROR = 5054, - X_API_KEY_FORBIDDEN_ERROR = 5055, - IS_ACTIVE_ERROR = 5056, - EXPIRED_ERROR = 5057, - NOT_FOUND_ERROR = 5058, -} diff --git a/src/common/api-key/dtos/api-key.payload.dto.ts b/src/common/api-key/dtos/api-key.payload.dto.ts deleted file mode 100644 index 582c805cd..000000000 --- a/src/common/api-key/dtos/api-key.payload.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { ApiProperty } from '@nestjs/swagger'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; - -export class ApiKeyPayloadDto extends DatabaseIdResponseDto { - @ApiProperty({ - description: 'Alias name of api key', - example: faker.person.jobTitle(), - required: true, - nullable: false, - }) - name: string; - - @ApiProperty({ - description: 'Type of api key', - example: ENUM_API_KEY_TYPE.PUBLIC, - required: true, - nullable: false, - }) - type: ENUM_API_KEY_TYPE; - - @ApiProperty({ - description: 'Unique key of api key', - example: faker.string.alpha(15), - required: true, - nullable: false, - }) - key: string; -} diff --git a/src/common/api-key/dtos/response/api-key.list.response.dto.ts b/src/common/api-key/dtos/response/api-key.list.response.dto.ts deleted file mode 100644 index ed649d597..000000000 --- a/src/common/api-key/dtos/response/api-key.list.response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; - -export class ApiKeyListResponseDto extends ApiKeyGetResponseDto {} diff --git a/src/common/api-key/dtos/response/api-key.reset.dto.ts b/src/common/api-key/dtos/response/api-key.reset.dto.ts deleted file mode 100644 index afb98b43c..000000000 --- a/src/common/api-key/dtos/response/api-key.reset.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ApiKeyCreateResponseDto } from 'src/common/api-key/dtos/response/api-key.create.dto'; - -export class ApiKeyResetResponseDto extends ApiKeyCreateResponseDto {} diff --git a/src/common/api-key/pipes/api-key.is-active.pipe.ts b/src/common/api-key/pipes/api-key.is-active.pipe.ts deleted file mode 100644 index 069773345..000000000 --- a/src/common/api-key/pipes/api-key.is-active.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; - -@Injectable() -export class ApiKeyActivePipe implements PipeTransform { - async transform(value: ApiKeyDoc): Promise { - if (!value.isActive) { - throw new BadRequestException({ - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, - message: 'apiKey.error.isActiveInvalid', - }); - } - - return value; - } -} - -@Injectable() -export class ApiKeyInactivePipe implements PipeTransform { - async transform(value: ApiKeyDoc): Promise { - if (value.isActive) { - throw new BadRequestException({ - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, - message: 'apiKey.error.isActiveInvalid', - }); - } - - return value; - } -} diff --git a/src/common/auth/constants/auth.status-code.constant.ts b/src/common/auth/constants/auth.status-code.constant.ts deleted file mode 100644 index bb1599e7a..000000000 --- a/src/common/auth/constants/auth.status-code.constant.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ENUM_AUTH_STATUS_CODE_ERROR { - JWT_ACCESS_TOKEN_ERROR = 5000, - JWT_REFRESH_TOKEN_ERROR = 5001, - SOCIAL_GOOGLE_ERROR = 5002, - SOCIAL_APPLE_ERROR = 5003, -} diff --git a/src/common/auth/dtos/social/auth.social.apple-payload.dto.ts b/src/common/auth/dtos/social/auth.social.apple-payload.dto.ts deleted file mode 100644 index 783d63b34..000000000 --- a/src/common/auth/dtos/social/auth.social.apple-payload.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AuthSocialGooglePayloadDto } from 'src/common/auth/dtos/social/auth.social.google-payload.dto'; - -export class AuthSocialApplePayloadDto extends AuthSocialGooglePayloadDto {} diff --git a/src/common/common.module.ts b/src/common/common.module.ts index 78593c436..d82c7a932 100644 --- a/src/common/common.module.ts +++ b/src/common/common.module.ts @@ -6,11 +6,16 @@ import { DatabaseService } from 'src/common/database/services/database.service'; import { MessageModule } from 'src/common/message/message.module'; import { HelperModule } from 'src/common/helper/helper.module'; import { RequestModule } from 'src/common/request/request.module'; -import { PolicyModule } from 'src/common/policy/policy.module'; -import { AuthModule } from 'src/common/auth/auth.module'; -import { ConfigModule } from '@nestjs/config'; +import { PolicyModule } from 'src/modules/policy/policy.module'; +import { AuthModule } from 'src/modules/auth/auth.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import configs from 'src/configs'; -import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { ApiKeyModule } from 'src/modules/api-key/api-key.module'; +import { PaginationModule } from 'src/common/pagination/pagination.module'; +import { FileModule } from 'src/common/file/file.module'; +import { redisStore } from 'cache-manager-redis-store'; +import { CacheModule, CacheStore } from '@nestjs/cache-manager'; +import type { RedisClientOptions } from 'redis'; @Module({ controllers: [], @@ -31,12 +36,31 @@ import { ApiKeyModule } from 'src/common/api-key/api-key.module'; useFactory: (databaseService: DatabaseService) => databaseService.createOptions(), }), + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + isGlobal: true, + useFactory: async (configService: ConfigService) => ({ + store: (await redisStore({ + socket: { + host: configService.get('redis.host'), + port: configService.get('redis.port'), + tls: configService.get('redis.tls'), + }, + username: configService.get('redis.username'), + password: configService.get('redis.password'), + ttl: configService.get('redis.cached.ttl'), + })) as unknown as CacheStore, + }), + }), MessageModule.forRoot(), HelperModule.forRoot(), RequestModule.forRoot(), PolicyModule.forRoot(), AuthModule.forRoot(), ApiKeyModule.forRoot(), + PaginationModule.forRoot(), + FileModule.forRoot(), ], }) export class CommonModule {} diff --git a/src/common/database/abstracts/base/database.entity.abstract.ts b/src/common/database/abstracts/base/database.entity.abstract.ts deleted file mode 100644 index 6fddc5541..000000000 --- a/src/common/database/abstracts/base/database.entity.abstract.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - DATABASE_CREATED_AT_FIELD_NAME, - DATABASE_DELETED_AT_FIELD_NAME, - DATABASE_UPDATED_AT_FIELD_NAME, -} from 'src/common/database/constants/database.constant'; - -export abstract class DatabaseEntityAbstract { - abstract _id: T; - abstract [DATABASE_DELETED_AT_FIELD_NAME]?: Date; - abstract [DATABASE_CREATED_AT_FIELD_NAME]?: Date; - abstract [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; -} diff --git a/src/common/database/abstracts/base/database.repository.abstract.ts b/src/common/database/abstracts/base/database.repository.abstract.ts deleted file mode 100644 index 721855f0e..000000000 --- a/src/common/database/abstracts/base/database.repository.abstract.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'; -import { - IDatabaseCreateOptions, - IDatabaseExistOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseCreateManyOptions, - IDatabaseManyOptions, - IDatabaseSoftDeleteManyOptions, - IDatabaseRestoreManyOptions, - IDatabaseRawOptions, - IDatabaseGetTotalOptions, - IDatabaseSaveOptions, - IDatabaseFindOneLockOptions, - IDatabaseRawFindAllOptions, - IDatabaseRawGetTotalOptions, - IDatabaseJoin, -} from 'src/common/database/interfaces/database.interface'; - -export abstract class DatabaseRepositoryAbstract { - abstract findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - - abstract findAllDistinct( - fieldDistinct: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - - abstract findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - - abstract findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - - abstract findOneAndLock( - find: Record, - options?: IDatabaseFindOneLockOptions - ): Promise; - - abstract findOneByIdAndLock( - _id: string, - options?: IDatabaseFindOneLockOptions - ): Promise; - - abstract getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - - abstract exists( - find: Record, - options?: IDatabaseExistOptions - ): Promise; - - abstract create( - data: Dto, - options?: IDatabaseCreateOptions - ): Promise; - - abstract save( - repository: Entity, - options?: IDatabaseSaveOptions - ): Promise; - - abstract delete( - repository: Entity, - options?: IDatabaseSaveOptions - ): Promise; - - abstract softDelete( - repository: Entity, - options?: IDatabaseSaveOptions - ): Promise; - - abstract restore( - repository: Entity, - options?: IDatabaseSaveOptions - ): Promise; - - abstract createMany( - data: Dto[], - options?: IDatabaseCreateManyOptions - ): Promise; - - abstract deleteManyByIds( - _id: string[], - options?: IDatabaseManyOptions - ): Promise; - - abstract deleteMany( - find: Record, - options?: IDatabaseManyOptions - ): Promise; - - abstract softDeleteManyByIds( - _id: string[], - options?: IDatabaseSoftDeleteManyOptions - ): Promise; - - abstract softDeleteMany( - find: Record, - options?: IDatabaseSoftDeleteManyOptions - ): Promise; - - abstract restoreManyByIds( - _id: string[], - options?: IDatabaseRestoreManyOptions - ): Promise; - - abstract restoreMany( - find: Record, - options?: IDatabaseRestoreManyOptions - ): Promise; - - abstract updateMany( - find: Record, - data: Dto, - options?: IDatabaseManyOptions - ): Promise; - - abstract join( - repository: Entity, - joins: IDatabaseJoin | IDatabaseJoin[] - ): Promise; - - abstract updateManyRaw( - find: Record, - data: UpdateWithAggregationPipeline | UpdateQuery, - options?: IDatabaseManyOptions - ): Promise; - - abstract raw( - rawOperation: RawQuery, - options?: IDatabaseRawOptions - ): Promise; - - abstract rawFindAll( - rawOperation: RawQuery, - options?: IDatabaseRawFindAllOptions - ): Promise; - - abstract rawGetTotal( - rawOperation: RawQuery, - options?: IDatabaseRawGetTotalOptions - ): Promise; - - abstract model(): Promise; -} diff --git a/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts b/src/common/database/abstracts/database.entity.abstract.ts similarity index 51% rename from src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts rename to src/common/database/abstracts/database.entity.abstract.ts index 1cdbd47ea..313ce0249 100644 --- a/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts +++ b/src/common/database/abstracts/database.entity.abstract.ts @@ -1,12 +1,7 @@ -import { DatabaseEntityAbstract } from 'src/common/database/abstracts/base/database.entity.abstract'; -import { - DATABASE_CREATED_AT_FIELD_NAME, - DATABASE_DELETED_AT_FIELD_NAME, - DATABASE_UPDATED_AT_FIELD_NAME, -} from 'src/common/database/constants/database.constant'; import { DatabaseProp } from 'src/common/database/decorators/database.decorator'; import { v4 as uuidV4 } from 'uuid'; -export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseEntityAbstract { + +export abstract class DatabaseEntityAbstract { @DatabaseProp({ type: String, default: uuidV4, @@ -14,11 +9,11 @@ export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseEntityAbst _id: string; @DatabaseProp({ - required: false, + required: true, index: true, - type: Date, + default: false, }) - [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + deleted: boolean; @DatabaseProp({ required: false, @@ -26,7 +21,13 @@ export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseEntityAbst type: Date, default: new Date(), }) - [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + createdAt?: Date; + + @DatabaseProp({ + required: false, + index: true, + }) + createdBy?: string; @DatabaseProp({ required: false, @@ -34,5 +35,24 @@ export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseEntityAbst type: Date, default: new Date(), }) - [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; + updatedAt?: Date; + + @DatabaseProp({ + required: false, + index: true, + }) + updatedBy?: string; + + @DatabaseProp({ + required: false, + index: true, + type: Date, + }) + deletedAt?: Date; + + @DatabaseProp({ + required: false, + index: true, + }) + deletedBy?: string; } diff --git a/src/common/database/abstracts/database.repository.abstract.ts b/src/common/database/abstracts/database.repository.abstract.ts new file mode 100644 index 000000000..10d6579ab --- /dev/null +++ b/src/common/database/abstracts/database.repository.abstract.ts @@ -0,0 +1,579 @@ +import { + Model, + PipelineStage, + PopulateOptions, + UpdateQuery, + UpdateWithAggregationPipeline, +} from 'mongoose'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; +import { + IDatabaseAggregateOptions, + IDatabaseCreateManyOptions, + IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, + IDatabaseDeleteOptions, + IDatabaseDocument, + IDatabaseExistOptions, + IDatabaseFindAllAggregateOptions, + IDatabaseFindAllOptions, + IDatabaseGetTotalOptions, + IDatabaseOptions, + IDatabaseSaveOptions, + IDatabaseUpdateManyOptions, + IDatabaseUpdateOptions, +} from 'src/common/database/interfaces/database.interface'; +import MongoDB from 'mongodb'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { DatabaseSoftDeleteDto } from 'src/common/database/dtos/database.soft-delete.dto'; + +export abstract class DatabaseRepositoryAbstract< + Entity extends DatabaseEntityAbstract, + EntityDocument extends IDatabaseDocument, +> { + protected readonly _repository: Model; + readonly _join?: PopulateOptions | (string | PopulateOptions)[]; + + constructor( + repository: Model, + options?: PopulateOptions | (string | PopulateOptions)[] + ) { + this._repository = repository; + this._join = options; + } + + // Find + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const repository = this._repository.find({ + ...find, + deleted: options?.withDeleted ?? false, + }); + + if (options?.select) { + repository.select(options.select); + } + + if (options?.paging) { + repository.limit(options.paging.limit).skip(options.paging.offset); + } + + if (options?.order) { + repository.sort(options.order); + } + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository.exec(); + } + + async findOne( + find: Record, + options?: IDatabaseOptions + ): Promise { + const repository = this._repository.findOne({ + ...find, + deleted: options?.withDeleted ?? false, + }); + + if (options?.select) { + repository.select(options.select); + } + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository.exec(); + } + + async findOneById( + _id: string, + options?: IDatabaseOptions + ): Promise { + const repository = this._repository.findOne({ + _id, + deleted: options?.withDeleted ?? false, + }); + + if (options?.select) { + repository.select(options.select); + } + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository.exec(); + } + + async findOneAndLock( + find: Record, + options?: IDatabaseOptions + ): Promise { + const repository = this._repository.findOneAndUpdate( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.select) { + repository.select(options.select); + } + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository.exec(); + } + + async findOneByIdAndLock( + _id: string, + options?: IDatabaseOptions + ): Promise { + const repository = this._repository.findOneAndUpdate( + { + _id, + deleted: options?.withDeleted ?? false, + }, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.select) { + repository.select(options.select); + } + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository.exec(); + } + + async getTotal( + find?: Record, + options?: IDatabaseGetTotalOptions + ): Promise { + const repository = this._repository.countDocuments({ + ...find, + deleted: options?.withDeleted ?? false, + }); + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + return repository; + } + + async exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise { + if (options?.excludeId) { + find = { + ...find, + _id: { + $nin: options?.excludeId ?? [], + }, + }; + } + + const repository = this._repository.exists({ + ...find, + deleted: options?.withDeleted ?? false, + }); + + if (options?.join) { + repository.populate( + (typeof options.join === 'boolean' && options.join + ? this._join + : options.join) as + | PopulateOptions + | (string | PopulateOptions)[] + ); + } + + if (options?.session) { + repository.session(options.session); + } + + const result = await repository; + return result ? true : false; + } + + async create( + data: T, + options?: IDatabaseCreateOptions + ): Promise { + const created = await this._repository.create([data], options); + + return created[0] as any; + } + + // Action + async update( + find: Record, + data: UpdateQuery | UpdateWithAggregationPipeline, + options?: IDatabaseUpdateOptions + ): Promise { + return this._repository.findOneAndUpdate( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + data, + { + ...options, + new: true, + } + ); + } + + async delete( + find: Record, + options?: IDatabaseDeleteOptions + ): Promise { + return this._repository.findOneAndDelete( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + { + ...options, + new: false, + } + ); + } + + async save( + repository: EntityDocument, + options?: IDatabaseSaveOptions + ): Promise { + return repository.save(options); + } + + async join( + repository: EntityDocument, + joins: PopulateOptions | (string | PopulateOptions)[] + ): Promise { + return repository.populate(joins); + } + + // Soft delete + async softDelete( + repository: EntityDocument, + dto?: DatabaseSoftDeleteDto, + options?: IDatabaseOptions + ): Promise { + repository.deletedAt = new Date(); + repository.deleted = true; + repository.deletedBy = dto?.deletedBy; + + return repository.save(options); + } + + async restore( + repository: EntityDocument, + options?: IDatabaseSaveOptions + ): Promise { + repository.deletedAt = undefined; + repository.deleted = false; + repository.deletedBy = undefined; + + return repository.save(options); + } + + // Bulk + async createMany( + data: T[], + options?: IDatabaseCreateManyOptions + ): Promise { + return this._repository.insertMany(data, { + ...options, + rawResult: true, + }); + } + + async updateMany( + find: Record, + data: T, + options?: IDatabaseUpdateManyOptions + ): Promise { + return this._repository.updateMany( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + { + $set: data, + }, + { ...options, rawResult: true } + ); + } + + async updateManyRaw( + find: Record, + data: UpdateQuery | UpdateWithAggregationPipeline, + options?: IDatabaseUpdateManyOptions + ): Promise { + return this._repository.updateMany( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + data, + { ...options, rawResult: true } + ); + } + + async deleteMany( + find: Record, + options?: IDatabaseDeleteManyOptions + ): Promise { + return this._repository.deleteMany( + { + ...find, + deleted: options?.withDeleted ?? false, + }, + { ...options, rawResult: true } + ); + } + + async softDeleteMany( + find: Record, + dto?: DatabaseSoftDeleteDto, + options?: IDatabaseOptions + ): Promise { + return this._repository.updateMany( + { + ...find, + deleted: false, + }, + { + $set: { + deletedAt: new Date(), + deleted: true, + deletedBy: dto?.deletedBy, + }, + }, + { ...options, rawResult: true } + ); + } + + async restoreMany( + find: Record, + options?: IDatabaseOptions + ): Promise { + return this._repository.updateMany( + { + ...find, + deleted: true, + }, + { + $set: { + deletedAt: undefined, + deleted: false, + deletedBy: undefined, + }, + }, + { ...options, rawResult: true } + ); + } + + // Raw + async aggregate< + AggregatePipeline extends PipelineStage, + AggregateResponse = any, + >( + pipelines: AggregatePipeline[], + options?: IDatabaseAggregateOptions + ): Promise { + if (!Array.isArray(pipelines)) { + throw new Error('Must in array'); + } + + const newPipelines: PipelineStage[] = [ + { + $match: { + deleted: options?.withDeleted ?? false, + }, + }, + ...pipelines, + ]; + + const aggregate = + this._repository.aggregate(newPipelines); + + if (options?.session) { + aggregate.session(options?.session); + } + + return aggregate; + } + + async findAllAggregate< + AggregatePipeline extends PipelineStage, + AggregateResponse = any, + >( + pipelines: AggregatePipeline[], + options?: IDatabaseFindAllAggregateOptions + ): Promise { + if (!Array.isArray(pipelines)) { + throw new Error('Must in array'); + } + + const newPipelines: PipelineStage[] = [ + { + $match: { + deleted: options?.withDeleted ?? false, + }, + }, + ...pipelines, + ]; + + if (options?.order) { + const keysOrder = Object.keys(options?.order); + newPipelines.push({ + $sort: keysOrder.reduce( + (a, b) => ({ + ...a, + [b]: + options?.order[b] === + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC + ? 1 + : -1, + }), + {} + ), + }); + } + + if (options?.paging) { + newPipelines.push( + { + $limit: options.paging.limit + options.paging.offset, + }, + { $skip: options.paging.offset } + ); + } + + const aggregate = + this._repository.aggregate(newPipelines); + + if (options?.session) { + aggregate.session(options?.session); + } + + return aggregate; + } + + async getTotalAggregate( + pipelines: AggregatePipeline[], + options?: IDatabaseAggregateOptions + ): Promise { + if (!Array.isArray(pipelines)) { + throw new Error('Must in array'); + } + + const newPipelines: PipelineStage[] = [ + { + $match: { + deleted: options?.withDeleted ?? false, + }, + }, + ...pipelines, + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + ]; + + const aggregate = this._repository.aggregate(newPipelines); + + if (options?.session) { + aggregate.session(options?.session); + } + + const raw = await aggregate; + return raw && raw.length > 0 ? raw[0].count : 0; + } + + async model(): Promise> { + return this._repository; + } +} diff --git a/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts b/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts deleted file mode 100644 index 998ba910f..000000000 --- a/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts +++ /dev/null @@ -1,793 +0,0 @@ -import { UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'; -import { - ClientSession, - Model, - PipelineStage, - PopulateOptions, - Document, -} from 'mongoose'; -import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/base/database.repository.abstract'; -import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant'; -import { - IDatabaseCreateOptions, - IDatabaseExistOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, - IDatabaseCreateManyOptions, - IDatabaseManyOptions, - IDatabaseSoftDeleteManyOptions, - IDatabaseRestoreManyOptions, - IDatabaseRawOptions, - IDatabaseSaveOptions, - IDatabaseFindOneLockOptions, - IDatabaseRawFindAllOptions, - IDatabaseRawGetTotalOptions, - IDatabaseJoin, -} from 'src/common/database/interfaces/database.interface'; -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; - -export abstract class DatabaseMongoUUIDRepositoryAbstract< - Entity, - EntityDocument, -> extends DatabaseRepositoryAbstract { - protected _repository: Model; - protected _joinOnFind?: IDatabaseJoin | IDatabaseJoin[]; - - constructor( - repository: Model, - options?: IDatabaseJoin | IDatabaseJoin[] - ) { - super(); - - this._repository = repository; - this._joinOnFind = options; - } - - async findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - const findAll = this._repository.find(find); - - if (!options?.withDeleted) { - findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findAll.select(options.select); - } - - if (options?.paging) { - findAll.limit(options.paging.limit).skip(options.paging.offset); - } - - if (options?.order) { - findAll.sort(options.order); - } - - if (options?.join) { - findAll.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findAll.session(options.session); - } - - return findAll.exec(); - } - - async findAllDistinct( - fieldDistinct: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - const findAll = this._repository.distinct( - fieldDistinct, - find - ); - - if (!options?.withDeleted) { - findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findAll.select(options.select); - } - - if (options?.paging) { - findAll.limit(options.paging.limit).skip(options.paging.offset); - } - - if (options?.order) { - findAll.sort(options.order); - } - - if (options?.join) { - findAll.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findAll.session(options.session); - } - - return findAll.exec() as any; - } - async findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise { - const findOne = this._repository.findOne(find); - - if (!options?.withDeleted) { - findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findOne.select(options.select); - } - - if (options?.join) { - findOne.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findOne.session(options.session); - } - - return findOne.exec(); - } - - async findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise { - const findOne = this._repository.findById(_id); - - if (!options?.withDeleted) { - findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findOne.select(options.select); - } - - if (options?.join) { - findOne.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findOne.session(options.session); - } - - return findOne.exec(); - } - - async findOneAndLock( - find: Record, - options?: IDatabaseFindOneLockOptions - ): Promise { - const findOne = this._repository.findOneAndUpdate(find, { - new: true, - useFindAndModify: false, - }); - - if (!options?.withDeleted) { - findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findOne.select(options.select); - } - - if (options?.join) { - findOne.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findOne.session(options.session); - } - - return findOne.exec(); - } - - async findOneByIdAndLock( - _id: string, - options?: IDatabaseFindOneLockOptions - ): Promise { - const findOne = this._repository.findByIdAndUpdate(_id, { - new: true, - useFindAndModify: false, - }); - - if (!options?.withDeleted) { - findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.select) { - findOne.select(options.select); - } - - if (options?.join) { - findOne.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - if (options?.session) { - findOne.session(options.session); - } - - return findOne.exec(); - } - - async getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - const count = this._repository.countDocuments(find); - - if (!options?.withDeleted) { - count.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.session) { - count.session(options.session); - } - - if (options?.join) { - count.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - return count; - } - - async exists( - find: Record, - options?: IDatabaseExistOptions - ): Promise { - if (options?.excludeId) { - find = { - ...find, - _id: { - $nin: options?.excludeId ?? [], - }, - }; - } - - const exist = this._repository.exists(find); - if (!options?.withDeleted) { - exist.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); - } - - if (options?.session) { - exist.session(options.session); - } - - if (options?.join) { - exist.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - const result = await exist; - return result ? true : false; - } - - async create( - data: Dto, - options?: IDatabaseCreateOptions - ): Promise { - const dataCreate: Record = data; - - if (options?._id) { - dataCreate._id = options._id; - } - - const created = await this._repository.create([dataCreate], { - session: options ? options.session : undefined, - }); - - return created[0] as EntityDocument; - } - - async save( - repository: EntityDocument & Document, - options?: IDatabaseSaveOptions - ): Promise { - return repository.save(options); - } - - async delete( - repository: EntityDocument & Document, - options?: IDatabaseSaveOptions - ): Promise { - return repository.deleteOne(options); - } - - async softDelete( - repository: EntityDocument & Document & { deletedAt?: Date }, - options?: IDatabaseSaveOptions - ): Promise { - repository.deletedAt = new Date(); - return repository.save(options); - } - - async restore( - repository: EntityDocument & Document & { deletedAt?: Date }, - options?: IDatabaseSaveOptions - ): Promise { - repository.deletedAt = undefined; - return repository.save(options); - } - - // bulk - async createMany( - data: Dto[], - options?: IDatabaseCreateManyOptions - ): Promise { - const create = this._repository.insertMany(data, { - session: options ? options.session : undefined, - }); - - try { - await create; - return true; - } catch (err: unknown) { - throw err; - } - } - - async deleteManyByIds( - _id: string[], - options?: IDatabaseManyOptions - ): Promise { - const del = this._repository.deleteMany({ - _id: { - $in: _id, - } as any, - }); - - if (options?.session) { - del.session(options.session); - } - - if (options?.join) { - del.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await del; - return true; - } catch (err: unknown) { - throw err; - } - } - - async deleteMany( - find: Record, - options?: IDatabaseManyOptions - ): Promise { - const del = this._repository.deleteMany(find); - - if (options?.session) { - del.session(options.session); - } - - if (options?.join) { - del.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await del; - return true; - } catch (err: unknown) { - throw err; - } - } - - async softDeleteManyByIds( - _id: string[], - options?: IDatabaseSoftDeleteManyOptions - ): Promise { - const softDel = this._repository - .updateMany( - { - _id: { - $in: _id, - } as any, - }, - { - $set: { - deletedAt: new Date(), - }, - } - ) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(false); - - if (options?.session) { - softDel.session(options.session); - } - - if (options?.join) { - softDel.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await softDel; - return true; - } catch (err: unknown) { - throw err; - } - } - - async softDeleteMany( - find: Record, - options?: IDatabaseSoftDeleteManyOptions - ): Promise { - const softDel = this._repository - .updateMany(find, { - $set: { - deletedAt: new Date(), - }, - }) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(false); - - if (options?.session) { - softDel.session(options.session); - } - - if (options?.join) { - softDel.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await softDel; - return true; - } catch (err: unknown) { - throw err; - } - } - - async restoreManyByIds( - _id: string[], - options?: IDatabaseRestoreManyOptions - ): Promise { - const rest = this._repository - .updateMany( - { - _id: { - $in: _id, - } as any, - }, - { - $set: { - deletedAt: undefined, - }, - } - ) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(true); - - if (options?.session) { - rest.session(options.session); - } - - if (options?.join) { - rest.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await rest; - return true; - } catch (err: unknown) { - throw err; - } - } - - async restoreMany( - find: Record, - options?: IDatabaseRestoreManyOptions - ): Promise { - const rest = this._repository - .updateMany(find, { - $set: { - deletedAt: undefined, - }, - }) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(true); - - if (options?.session) { - rest.session(options.session); - } - - if (options?.join) { - rest.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await rest; - return true; - } catch (err: unknown) { - throw err; - } - } - - async updateMany( - find: Record, - data: Dto, - options?: IDatabaseManyOptions - ): Promise { - const update = this._repository - .updateMany(find, { - $set: data, - }) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(false); - - if (options?.session) { - update.session(options.session as ClientSession); - } - - if (options?.join) { - update.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await update; - return true; - } catch (err: unknown) { - throw err; - } - } - - async join( - repository: EntityDocument & Document, - joins: IDatabaseJoin | IDatabaseJoin[] - ): Promise { - const cOptions = this._convertJoinOption(joins); - - return repository.populate(cOptions); - } - - // raw - async updateManyRaw( - find: Record, - data: UpdateWithAggregationPipeline | UpdateQuery, - options?: IDatabaseManyOptions - ): Promise { - const update = this._repository - .updateMany(find, data) - .where(DATABASE_DELETED_AT_FIELD_NAME) - .exists(false); - - if (options?.session) { - update.session(options.session as ClientSession); - } - - if (options?.join) { - update.populate( - typeof options.join === 'boolean' - ? this._convertJoinOption(this._joinOnFind) - : this._convertJoinOption(options.join) - ); - } - - try { - await update; - return true; - } catch (err: unknown) { - throw err; - } - } - - async raw( - rawOperation: RawQuery, - options?: IDatabaseRawOptions - ): Promise { - if (!Array.isArray(rawOperation)) { - throw new Error('Must in array'); - } - - const pipeline: PipelineStage[] = rawOperation; - - if (!options?.withDeleted) { - pipeline.push({ - $match: { - [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, - }, - }); - } - - const aggregate = this._repository.aggregate(pipeline); - - if (options?.session) { - aggregate.session(options?.session); - } - - return aggregate; - } - - async rawFindAll( - rawOperation: RawQuery, - options?: IDatabaseRawFindAllOptions - ): Promise { - if (!Array.isArray(rawOperation)) { - throw new Error('Must in array'); - } - - const pipeline: PipelineStage[] = rawOperation; - if (!options?.withDeleted) { - pipeline.push({ - $match: { - [DATABASE_DELETED_AT_FIELD_NAME]: { - $exists: false, - }, - }, - }); - } - - if (options?.order) { - const keysOrder = Object.keys(options?.order); - pipeline.push({ - $sort: keysOrder.reduce( - (a, b) => ({ - ...a, - [b]: - options?.order[b] === - ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC - ? 1 - : -1, - }), - {} - ), - }); - } - - if (options?.paging) { - pipeline.push( - { - $limit: options.paging.limit + options.paging.offset, - }, - { $skip: options.paging.offset } - ); - } - - const aggregate = this._repository.aggregate(pipeline); - - if (options?.session) { - aggregate.session(options?.session); - } - - return aggregate; - } - - async rawGetTotal( - rawOperation: RawQuery, - options?: IDatabaseRawGetTotalOptions - ): Promise { - if (!Array.isArray(rawOperation)) { - throw new Error('Must in array'); - } - - const pipeline: PipelineStage[] = rawOperation; - pipeline.push({ - $group: { - _id: null, - count: { $sum: 1 }, - }, - }); - - const aggregate = this._repository.aggregate(pipeline); - - if (options?.session) { - aggregate.session(options?.session); - } - - const raw = await aggregate; - return raw && raw.length > 0 ? raw[0].count : 0; - } - - async model(): Promise> { - return this._repository; - } - - private _convertJoinOption( - options: IDatabaseJoin | IDatabaseJoin[] - ): PopulateOptions | PopulateOptions[] { - if (Array.isArray(options)) { - const cOptions: PopulateOptions[] = options.map(e => { - const aOptions: PopulateOptions = { - path: e.field, - foreignField: e.foreignKey, - localField: e.localKey, - model: e.model, - match: e.condition, - }; - - if (e.justOne) { - aOptions.justOne = true; - aOptions.perDocumentLimit = 1; - } - - return aOptions; - }); - - return cOptions; - } - - const cOptions: PopulateOptions = { - path: options.field, - foreignField: options.foreignKey, - localField: options.localKey, - model: options.model, - match: options.condition, - }; - - if (options.justOne) { - cOptions.justOne = true; - cOptions.perDocumentLimit = 1; - } - - return cOptions; - } -} diff --git a/src/common/database/constants/database.constant.ts b/src/common/database/constants/database.constant.ts index 3a992e26a..596584d71 100644 --- a/src/common/database/constants/database.constant.ts +++ b/src/common/database/constants/database.constant.ts @@ -1,5 +1 @@ export const DATABASE_CONNECTION_NAME = 'PrimaryConnectionDatabase'; - -export const DATABASE_DELETED_AT_FIELD_NAME = 'deletedAt'; -export const DATABASE_UPDATED_AT_FIELD_NAME = 'updatedAt'; -export const DATABASE_CREATED_AT_FIELD_NAME = 'createdAt'; diff --git a/src/common/database/decorators/database.decorator.ts b/src/common/database/decorators/database.decorator.ts index b5c9f2206..3e5865a3e 100644 --- a/src/common/database/decorators/database.decorator.ts +++ b/src/common/database/decorators/database.decorator.ts @@ -9,11 +9,7 @@ import { SchemaOptions, } from '@nestjs/mongoose'; import { Schema as MongooseSchema } from 'mongoose'; -import { - DATABASE_CONNECTION_NAME, - DATABASE_CREATED_AT_FIELD_NAME, - DATABASE_UPDATED_AT_FIELD_NAME, -} from 'src/common/database/constants/database.constant'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; import { IDatabaseQueryContainOptions } from 'src/common/database/interfaces/database.interface'; export function DatabaseConnection( @@ -32,12 +28,9 @@ export function DatabaseModel( export function DatabaseEntity(options?: SchemaOptions): ClassDecorator { return Schema({ ...options, - versionKey: false, - autoCreate: false, - autoIndex: false, timestamps: options?.timestamps ?? { - createdAt: DATABASE_CREATED_AT_FIELD_NAME, - updatedAt: DATABASE_UPDATED_AT_FIELD_NAME, + createdAt: true, + updatedAt: true, }, }); } diff --git a/src/common/database/dtos/database.dto.ts b/src/common/database/dtos/database.dto.ts new file mode 100644 index 000000000..3db52163c --- /dev/null +++ b/src/common/database/dtos/database.dto.ts @@ -0,0 +1,49 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DatabaseDto { + @ApiProperty({ + description: 'Alias id of api key', + example: faker.string.uuid(), + required: true, + }) + _id: string; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + nullable: false, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: true, + nullable: false, + }) + updatedAt: Date; + + @ApiProperty({ + description: 'Flag for deleted', + default: false, + required: true, + nullable: false, + }) + deleted: boolean; + + @ApiProperty({ + description: 'Date delete at', + required: false, + nullable: true, + }) + deletedAt?: Date; + + @ApiProperty({ + description: 'Delete by', + required: false, + nullable: true, + }) + deletedBy?: string; +} diff --git a/src/modules/user/dtos/request/user-login-history.create.request.dto.ts b/src/common/database/dtos/database.soft-delete.dto.ts similarity index 53% rename from src/modules/user/dtos/request/user-login-history.create.request.dto.ts rename to src/common/database/dtos/database.soft-delete.dto.ts index d06b44161..842cfbd9e 100644 --- a/src/modules/user/dtos/request/user-login-history.create.request.dto.ts +++ b/src/common/database/dtos/database.soft-delete.dto.ts @@ -1,13 +1,15 @@ import { faker } from '@faker-js/faker'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; -export class UserLoginHistoryCreateRequest { +export class DatabaseSoftDeleteDto { @ApiProperty({ + description: 'Alias id of api key', example: faker.string.uuid(), required: true, }) @IsNotEmpty() + @IsString() @IsUUID() - readonly user: string; + deletedBy: string; } diff --git a/src/common/database/dtos/response/database.id.response.dto.ts b/src/common/database/dtos/response/database.id.response.dto.ts index a6e706d3c..cdc4cc8aa 100644 --- a/src/common/database/dtos/response/database.id.response.dto.ts +++ b/src/common/database/dtos/response/database.id.response.dto.ts @@ -1,11 +1,6 @@ -import { faker } from '@faker-js/faker'; -import { ApiProperty } from '@nestjs/swagger'; +import { PickType } from '@nestjs/swagger'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; -export class DatabaseIdResponseDto { - @ApiProperty({ - description: 'Alias id of api key', - example: faker.string.uuid(), - required: true, - }) - _id: string; -} +export class DatabaseIdResponseDto extends PickType(DatabaseDto, [ + '_id', +] as const) {} diff --git a/src/common/database/interfaces/database.interface.ts b/src/common/database/interfaces/database.interface.ts index 2a5a3e220..e60278c1c 100644 --- a/src/common/database/interfaces/database.interface.ts +++ b/src/common/database/interfaces/database.interface.ts @@ -1,99 +1,59 @@ -import { Document } from 'mongoose'; +import { ClientSession, Document, PopulateOptions } from 'mongoose'; import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; export interface IDatabaseQueryContainOptions { fullWord: boolean; } -export interface IDatabaseJoin { - field: string; - localKey: string; - foreignKey: string; - model: any; - condition?: Record; - justOne?: boolean; - join?: this | this[]; -} - export type IDatabaseDocument = T & Document; -// find one -export interface IDatabaseFindOneOptions { +// Find +export interface IDatabaseOptions { select?: Record | string; - join?: boolean | IDatabaseJoin | IDatabaseJoin[]; - session?: T; + join?: boolean | PopulateOptions | PopulateOptions[]; + session?: ClientSession; withDeleted?: boolean; } -// find one lock -export type IDatabaseFindOneLockOptions = IDatabaseFindOneOptions; +export type IDatabaseGetTotalOptions = Omit; -export type IDatabaseGetTotalOptions = Pick< - IDatabaseFindOneOptions, - 'session' | 'withDeleted' | 'join' ->; - -export type IDatabaseSaveOptions = Pick< - IDatabaseFindOneOptions, - 'session' ->; - -// find -export interface IDatabaseFindAllPaginationPagingOptions { +export interface IDatabaseFindAllPagingOptions { limit: number; offset: number; } -export interface IDatabaseFindAllPaginationOptions { - paging?: IDatabaseFindAllPaginationPagingOptions; - order?: IPaginationOrder; -} -export interface IDatabaseFindAllOptions - extends IDatabaseFindAllPaginationOptions, - IDatabaseFindOneOptions {} - -// create -export interface IDatabaseCreateOptions - extends Pick, 'session'> { - _id?: string; +export interface IDatabaseFindAllOptions extends IDatabaseOptions { + paging: IDatabaseFindAllPagingOptions; + order: IPaginationOrder; } -// exist -export interface IDatabaseExistOptions - extends Pick< - IDatabaseFindOneOptions, - 'session' | 'withDeleted' | 'join' - > { +export interface IDatabaseExistOptions extends IDatabaseOptions { excludeId?: string[]; } -// bulk -export type IDatabaseManyOptions = Pick< - IDatabaseFindOneOptions, - 'session' | 'join' ->; - -export type IDatabaseCreateManyOptions = Pick< - IDatabaseFindOneOptions, - 'session' +// Action +export type IDatabaseCreateOptions = Pick; +export type IDatabaseUpdateOptions = Omit; +export type IDatabaseDeleteOptions = Omit; +export type IDatabaseSaveOptions = Pick; + +// Bulk +export type IDatabaseCreateManyOptions = Pick; +export interface IDatabaseUpdateManyOptions + extends Pick { + upsert?: boolean; +} +export type IDatabaseDeleteManyOptions = Pick< + IDatabaseOptions, + 'session' | 'withDeleted' >; -export type IDatabaseSoftDeleteManyOptions = IDatabaseManyOptions; - -export type IDatabaseRestoreManyOptions = IDatabaseManyOptions; - // Raw -export type IDatabaseRawOptions = Pick< - IDatabaseFindOneOptions, +export type IDatabaseAggregateOptions = Pick< + IDatabaseOptions, 'session' | 'withDeleted' >; - -export type IDatabaseRawFindAllOptions = Pick< - IDatabaseFindAllOptions, - 'order' | 'paging' | 'session' | 'withDeleted' ->; - -export type IDatabaseRawGetTotalOptions = Pick< - IDatabaseRawFindAllOptions, - 'session' | 'withDeleted' +export type IDatabaseFindAllAggregateOptions = Omit< + IDatabaseFindAllOptions, + 'join' | 'select' >; diff --git a/src/common/database/services/database.service.ts b/src/common/database/services/database.service.ts index 25393b0d5..59d56f676 100644 --- a/src/common/database/services/database.service.ts +++ b/src/common/database/services/database.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { MongooseModuleOptions } from '@nestjs/mongoose'; import mongoose from 'mongoose'; import { ConfigService } from '@nestjs/config'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; import { IDatabaseService } from 'src/common/database/interfaces/database.service.interface'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; @Injectable() export class DatabaseService implements IDatabaseService { @@ -25,8 +25,8 @@ export class DatabaseService implements IDatabaseService { const mongooseOptions: MongooseModuleOptions = { uri, - autoCreate: true, - autoIndex: true, + autoCreate: env === ENUM_APP_ENVIRONMENT.MIGRATION, + autoIndex: env === ENUM_APP_ENVIRONMENT.MIGRATION, ...timeoutOptions, }; diff --git a/src/common/doc/decorators/doc.decorator.ts b/src/common/doc/decorators/doc.decorator.ts index 634d6960c..ba99a3de6 100644 --- a/src/common/doc/decorators/doc.decorator.ts +++ b/src/common/doc/decorators/doc.decorator.ts @@ -13,7 +13,6 @@ import { ApiSecurity, getSchemaPath, } from '@nestjs/swagger'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { IDocAuthOptions, IDocDefaultOptions, @@ -25,16 +24,17 @@ import { IDocResponseFileOptions, IDocResponseOptions, } from 'src/common/doc/interfaces/doc.interface'; -import { ENUM_FILE_MIME } from 'src/common/file/constants/file.enum.constant'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; -import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; import { ResponseDto } from 'src/common/response/dtos/response.dto'; import { ResponsePagingDto } from 'src/common/response/dtos/response.paging.dto'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; -import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/common/policy/constants/policy.status-code.constant'; -import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/constants/app.status-code.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; export function DocDefault(options: IDocDefaultOptions): MethodDecorator { const docs = []; @@ -236,12 +236,12 @@ export function Doc(options?: IDocOptions): MethodDecorator { DocDefault({ httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, messagePath: 'http.serverError.internalServerError', - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, }), DocDefault({ httpStatus: HttpStatus.REQUEST_TIMEOUT, messagePath: 'http.serverError.requestTimeout', - statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT_ERROR, + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, }) ); } @@ -261,7 +261,7 @@ export function DocRequest(options?: IDocRequestOptions) { docs.push( DocDefault({ httpStatus: HttpStatus.UNPROCESSABLE_ENTITY, - statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION_ERROR, + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION, messagePath: 'request.validation', }) ); @@ -317,14 +317,14 @@ export function DocGuard(options?: IDocGuardOptions) { if (options?.role) { oneOfForbidden.push({ - statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN_ERROR, + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN, messagePath: 'policy.error.roleForbidden', }); } if (options?.policy) { oneOfForbidden.push({ - statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN_ERROR, + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN, messagePath: 'policy.error.abilityForbidden', }); } @@ -340,7 +340,7 @@ export function DocAuth(options?: IDocAuthOptions) { docs.push(ApiBearerAuth('refreshToken')); oneOfUnauthorized.push({ messagePath: 'auth.error.refreshTokenUnauthorized', - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN, }); } @@ -348,7 +348,7 @@ export function DocAuth(options?: IDocAuthOptions) { docs.push(ApiBearerAuth('accessToken')); oneOfUnauthorized.push({ messagePath: 'auth.error.accessTokenUnauthorized', - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN, }); } @@ -356,7 +356,7 @@ export function DocAuth(options?: IDocAuthOptions) { docs.push(ApiBearerAuth('google')); oneOfUnauthorized.push({ messagePath: 'auth.error.socialGoogle', - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, }); } @@ -364,7 +364,7 @@ export function DocAuth(options?: IDocAuthOptions) { docs.push(ApiBearerAuth('apple')); oneOfUnauthorized.push({ messagePath: 'auth.error.socialApple', - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_APPLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_APPLE, }); } @@ -372,28 +372,23 @@ export function DocAuth(options?: IDocAuthOptions) { docs.push(ApiSecurity('xApiKey')); oneOfUnauthorized.push( { - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED, messagePath: 'apiKey.error.xApiKey.required', }, { - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND, messagePath: 'apiKey.error.xApiKey.notFound', }, { - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED, messagePath: 'apiKey.error.xApiKey.expired', }, { - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID, messagePath: 'apiKey.error.xApiKey.invalid', }, { - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN, messagePath: 'apiKey.error.xApiKey.forbidden', } ); diff --git a/src/common/doc/constants/doc.enum.constant.ts b/src/common/doc/enums/doc.enum.ts similarity index 100% rename from src/common/doc/constants/doc.enum.constant.ts rename to src/common/doc/enums/doc.enum.ts diff --git a/src/common/doc/interfaces/doc.interface.ts b/src/common/doc/interfaces/doc.interface.ts index 8345ca7df..97063ff14 100644 --- a/src/common/doc/interfaces/doc.interface.ts +++ b/src/common/doc/interfaces/doc.interface.ts @@ -1,8 +1,8 @@ import { HttpStatus } from '@nestjs/common'; import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger'; import { ClassConstructor } from 'class-transformer'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; -import { ENUM_FILE_MIME } from 'src/common/file/constants/file.enum.constant'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; export interface IDocOptions { summary?: string; @@ -36,8 +36,7 @@ export interface IDocRequestOptions { dto?: ClassConstructor; } -export interface IDocRequestFileOptions - extends Omit {} +export type IDocRequestFileOptions = Omit; export interface IDocGuardOptions { policy?: boolean; @@ -50,16 +49,7 @@ export interface IDocResponseOptions { dto?: ClassConstructor; } -export interface IDocResponsePagingOptions - extends Omit, 'dto'> { - dto: ClassConstructor; -} - export interface IDocResponseFileOptions extends Omit { fileType?: ENUM_FILE_MIME; } - -export interface IDocErrorOptions extends IDocResponseOptions { - messagePath: string; -} diff --git a/src/common/file/constants/file.status-code.constant.ts b/src/common/file/constants/file.status-code.constant.ts deleted file mode 100644 index 09573b8dd..000000000 --- a/src/common/file/constants/file.status-code.constant.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum ENUM_FILE_STATUS_CODE_ERROR { - REQUIRED_ERROR = 5020, - MAX_SIZE_ERROR = 5021, - MIME_ERROR = 5022, - MAX_FILES_ERROR = 5023, - VALIDATION_DTO_ERROR = 5024, - REQUIRED_EXTRACT_FIRST_ERROR = 5025, -} diff --git a/src/common/file/decorators/file.decorator.ts b/src/common/file/decorators/file.decorator.ts index c9b1b886e..a4db473f7 100644 --- a/src/common/file/decorators/file.decorator.ts +++ b/src/common/file/decorators/file.decorator.ts @@ -74,6 +74,8 @@ export const FilePartNumber: () => ParameterDecorator = createParamDecorator( (_: unknown, ctx: ExecutionContext): number => { const request = ctx.switchToHttp().getRequest(); const { headers } = request; - return Number(headers['x-part-number']) ?? 0; + return headers['x-part-number'] + ? Number(headers['x-part-number']) + : undefined; } ); diff --git a/src/common/file/constants/file.enum.constant.ts b/src/common/file/enums/file.enum.ts similarity index 100% rename from src/common/file/constants/file.enum.constant.ts rename to src/common/file/enums/file.enum.ts diff --git a/src/common/file/enums/file.status-code.enum.ts b/src/common/file/enums/file.status-code.enum.ts new file mode 100644 index 000000000..602001464 --- /dev/null +++ b/src/common/file/enums/file.status-code.enum.ts @@ -0,0 +1,8 @@ +export enum ENUM_FILE_STATUS_CODE_ERROR { + REQUIRED = 5020, + MAX_SIZE = 5021, + MIME_INVALID = 5022, + MAX_FILES = 5023, + VALIDATION_DTO = 5024, + REQUIRED_EXTRACT_FIRST = 5025, +} diff --git a/src/common/file/exceptions/file.import.exception.ts b/src/common/file/exceptions/file.import.exception.ts index 034ba7eeb..166186466 100644 --- a/src/common/file/exceptions/file.import.exception.ts +++ b/src/common/file/exceptions/file.import.exception.ts @@ -1,16 +1,15 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; -import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { HttpStatus } from '@nestjs/common'; import { IMessageValidationImportErrorParam } from 'src/common/message/interfaces/message.interface'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; + +export class FileImportException extends Error { + readonly httpStatus: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY; + readonly statusCode: number = ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION; + readonly errors: IMessageValidationImportErrorParam[]; -export class FileImportException extends HttpException { constructor(errors: IMessageValidationImportErrorParam[]) { - super( - { - statusCode: ENUM_FILE_STATUS_CODE_ERROR.VALIDATION_DTO_ERROR, - message: 'file.error.validationDto', - errors, - }, - HttpStatus.UNPROCESSABLE_ENTITY - ); + super('file.error.validationDto'); + + this.errors = errors; } } diff --git a/src/common/file/file.module.ts b/src/common/file/file.module.ts index d7d22dd3c..7ca06f5da 100644 --- a/src/common/file/file.module.ts +++ b/src/common/file/file.module.ts @@ -1,10 +1,16 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Global, Module } from '@nestjs/common'; import { FileService } from 'src/common/file/services/file.service'; -@Module({ - providers: [FileService], - exports: [FileService], - controllers: [], - imports: [], -}) -export class FileModule {} +@Global() +@Module({}) +export class FileModule { + static forRoot(): DynamicModule { + return { + module: FileModule, + providers: [FileService], + exports: [FileService], + imports: [], + controllers: [], + }; + } +} diff --git a/src/common/file/interfaces/file.interface.ts b/src/common/file/interfaces/file.interface.ts index ecdd69694..63ed991ec 100644 --- a/src/common/file/interfaces/file.interface.ts +++ b/src/common/file/interfaces/file.interface.ts @@ -5,10 +5,6 @@ export interface IFileRows { sheetName?: string; } -export interface IFileReadOptions { - password?: string; -} - export interface IFileUploadSingle { field: string; fileSize: number; @@ -18,8 +14,9 @@ export interface IFileUploadMultiple extends IFileUploadSingle { maxFiles: number; } -export interface IFileUploadMultipleField - extends Omit {} +export type IFileUploadMultipleField = Omit; -export interface IFileUploadMultipleFieldOptions - extends Pick {} +export type IFileUploadMultipleFieldOptions = Pick< + IFileUploadSingle, + 'fileSize' +>; diff --git a/src/common/file/interfaces/file.service.interface.ts b/src/common/file/interfaces/file.service.interface.ts index 76920afe8..8bc15f238 100644 --- a/src/common/file/interfaces/file.service.interface.ts +++ b/src/common/file/interfaces/file.service.interface.ts @@ -1,19 +1,10 @@ -import { - IFileReadOptions, - IFileRows, -} from 'src/common/file/interfaces/file.interface'; +import { IFileRows } from 'src/common/file/interfaces/file.interface'; export interface IFileService { writeCsv(rows: IFileRows): Buffer; writeCsvFromArray(rows: T[][]): Buffer; - writeExcel( - rows: IFileRows[], - options?: IFileReadOptions - ): Buffer; - writeExcelFromArray( - rows: T[][], - options?: IFileReadOptions - ): Buffer; + writeExcel(rows: IFileRows[]): Buffer; + writeExcelFromArray(rows: T[][]): Buffer; readCsv(file: Buffer): IFileRows; - readExcel(file: Buffer, options?: IFileReadOptions): IFileRows[]; + readExcel(file: Buffer): IFileRows[]; } diff --git a/src/common/file/pipes/file.excel-extract.pipe.ts b/src/common/file/pipes/file.excel-extract.pipe.ts index 9b08fb3e4..d64c176ae 100644 --- a/src/common/file/pipes/file.excel-extract.pipe.ts +++ b/src/common/file/pipes/file.excel-extract.pipe.ts @@ -3,8 +3,8 @@ import { PipeTransform } from '@nestjs/common/interfaces'; import { ENUM_FILE_MIME, ENUM_FILE_MIME_EXCEL, -} from 'src/common/file/constants/file.enum.constant'; -import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +} from 'src/common/file/enums/file.enum'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; import { IFile, IFileRows } from 'src/common/file/interfaces/file.interface'; import { FileService } from 'src/common/file/services/file.service'; @@ -29,7 +29,7 @@ export class FileExcelParsePipe implements PipeTransform { if (!supportedFiles.includes(mimetype)) { throw new UnsupportedMediaTypeException({ - statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_ERROR, + statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_INVALID, message: 'file.error.mimeInvalid', }); } diff --git a/src/common/file/pipes/file.excel-validation.pipe.ts b/src/common/file/pipes/file.excel-validation.pipe.ts index a6aadc887..532f1ead1 100644 --- a/src/common/file/pipes/file.excel-validation.pipe.ts +++ b/src/common/file/pipes/file.excel-validation.pipe.ts @@ -1,18 +1,20 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { PipeTransform } from '@nestjs/common/interfaces'; -import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; import { IMessageValidationImportErrorParam } from 'src/common/message/interfaces/message.interface'; import { FileImportException } from 'src/common/file/exceptions/file.import.exception'; import { plainToInstance } from 'class-transformer'; import { ValidationError, validate } from 'class-validator'; import { IFileRows } from 'src/common/file/interfaces/file.interface'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; //! only for excel and use after FileParsePipe @Injectable() -export class FileExcelValidationPipe implements PipeTransform { +export class FileExcelValidationPipe> + implements PipeTransform +{ constructor(private readonly dto: any) {} - async transform(value: IFileRows[]): Promise[]> { + async transform(value: IFileRows[]): Promise[]> { if (!value) { return; } @@ -23,11 +25,10 @@ export class FileExcelValidationPipe implements PipeTransform { return dtos; } - async validate(value: IFileRows[]): Promise { + async validate(value: IFileRows[]): Promise { if (!value || value.length === 0) { throw new UnprocessableEntityException({ - statusCode: - ENUM_FILE_STATUS_CODE_ERROR.REQUIRED_EXTRACT_FIRST_ERROR, + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED_EXTRACT_FIRST, message: 'file.error.requiredParseFirst', }); } @@ -36,7 +37,7 @@ export class FileExcelValidationPipe implements PipeTransform { } async validateParse( - value: IFileRows[], + value: IFileRows[], classDtos: any ): Promise[]> { const errors: IMessageValidationImportErrorParam[] = []; @@ -50,7 +51,7 @@ export class FileExcelValidationPipe implements PipeTransform { errors.push({ row: index, sheetName: parse.sheetName, - error: validator, + errors: validator, }); } else { dtos.push({ diff --git a/src/common/file/pipes/file.required.pipe.ts b/src/common/file/pipes/file.required.pipe.ts index 830aea1f9..a71382918 100644 --- a/src/common/file/pipes/file.required.pipe.ts +++ b/src/common/file/pipes/file.required.pipe.ts @@ -3,7 +3,7 @@ import { Injectable, UnprocessableEntityException, } from '@nestjs/common'; -import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; import { IFile } from 'src/common/file/interfaces/file.interface'; @Injectable() @@ -24,11 +24,11 @@ export class FileRequiredPipe implements PipeTransform { async validate(value: IFile | IFile[]): Promise { if ( !value || - Object.keys(value).length === 0 || - (Array.isArray(value) && value.length === 0) + (Array.isArray(value) && value.length === 0) || + Object.keys(value).length === 0 ) { throw new UnprocessableEntityException({ - statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED_ERROR, + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED, message: 'file.error.required', }); } diff --git a/src/common/file/pipes/file.type.pipe.ts b/src/common/file/pipes/file.type.pipe.ts index 29c4b5c46..500213c13 100644 --- a/src/common/file/pipes/file.type.pipe.ts +++ b/src/common/file/pipes/file.type.pipe.ts @@ -3,8 +3,8 @@ import { Injectable, UnsupportedMediaTypeException, } from '@nestjs/common'; -import { ENUM_FILE_MIME } from 'src/common/file/constants/file.enum.constant'; -import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; import { IFile } from 'src/common/file/interfaces/file.interface'; @Injectable() @@ -49,7 +49,7 @@ export class FileTypePipe implements PipeTransform { async validate(mimetype: string): Promise { if (!this.type.find(val => val === mimetype.toLowerCase())) { throw new UnsupportedMediaTypeException({ - statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_ERROR, + statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_INVALID, message: 'file.error.mimeInvalid', }); } diff --git a/src/common/file/services/file.service.ts b/src/common/file/services/file.service.ts index 5cfb5822a..65a13be30 100644 --- a/src/common/file/services/file.service.ts +++ b/src/common/file/services/file.service.ts @@ -1,10 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { - IFileReadOptions, - IFileRows, -} from 'src/common/file/interfaces/file.interface'; +import { IFileRows } from 'src/common/file/interfaces/file.interface'; import { IFileService } from 'src/common/file/interfaces/file.service.interface'; -import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; import { utils, write, read } from 'xlsx'; @Injectable() @@ -29,10 +26,7 @@ export class FileService implements IFileService { return buff; } - writeExcel( - rows: IFileRows[], - options?: IFileReadOptions - ): Buffer { + writeExcel(rows: IFileRows[]): Buffer { // workbook const workbook = utils.book_new(); @@ -50,16 +44,12 @@ export class FileService implements IFileService { const buff: Buffer = write(workbook, { type: 'buffer', bookType: ENUM_HELPER_FILE_EXCEL_TYPE.XLSX, - password: options?.password, }); return buff; } - writeExcelFromArray( - rows: T[][], - options?: IFileReadOptions - ): Buffer { + writeExcelFromArray(rows: T[][]): Buffer { // workbook const workbook = utils.book_new(); @@ -71,7 +61,6 @@ export class FileService implements IFileService { const buff: Buffer = write(workbook, { type: 'buffer', bookType: ENUM_HELPER_FILE_EXCEL_TYPE.XLSX, - password: options?.password, }); return buff; @@ -95,11 +84,10 @@ export class FileService implements IFileService { }; } - readExcel(file: Buffer, options?: IFileReadOptions): IFileRows[] { + readExcel(file: Buffer): IFileRows[] { // workbook const workbook = read(file, { type: 'buffer', - password: options?.password, }); // worksheet diff --git a/src/common/helper/constants/helper.enum.constant.ts b/src/common/helper/enums/helper.enum.ts similarity index 100% rename from src/common/helper/constants/helper.enum.constant.ts rename to src/common/helper/enums/helper.enum.ts diff --git a/src/common/helper/interfaces/helper.interface.ts b/src/common/helper/interfaces/helper.interface.ts index fcd46283a..2fe536463 100644 --- a/src/common/helper/interfaces/helper.interface.ts +++ b/src/common/helper/interfaces/helper.interface.ts @@ -1,7 +1,7 @@ import { ENUM_HELPER_DATE_DIFF, ENUM_HELPER_DATE_FORMAT, -} from 'src/common/helper/constants/helper.enum.constant'; +} from 'src/common/helper/enums/helper.enum'; // Helper Encryption export interface IHelperJwtVerifyOptions { @@ -19,9 +19,6 @@ export interface IHelperJwtOptions } // Helper String -export interface IHelperStringRandomOptions { - safe?: boolean; -} export interface IHelperStringCurrencyOptions { locale?: string; diff --git a/src/common/helper/interfaces/helper.string-service.interface.ts b/src/common/helper/interfaces/helper.string-service.interface.ts index c4663bbb7..3412033b1 100644 --- a/src/common/helper/interfaces/helper.string-service.interface.ts +++ b/src/common/helper/interfaces/helper.string-service.interface.ts @@ -1,12 +1,11 @@ import { IHelperStringCurrencyOptions, IHelperStringPasswordOptions, - IHelperStringRandomOptions, } from 'src/common/helper/interfaces/helper.interface'; export interface IHelperStringService { randomReference(length: number): string; - random(length: number, options?: IHelperStringRandomOptions): string; + random(length: number): string; censor(text: string): string; checkEmail(email: string): boolean; checkPasswordStrength( diff --git a/src/common/helper/services/helper.date.service.ts b/src/common/helper/services/helper.date.service.ts index a8a3f6a70..b8481ced7 100644 --- a/src/common/helper/services/helper.date.service.ts +++ b/src/common/helper/services/helper.date.service.ts @@ -4,7 +4,7 @@ import moment, { ISO_8601 } from 'moment-timezone'; import { ENUM_HELPER_DATE_DIFF, ENUM_HELPER_DATE_FORMAT, -} from 'src/common/helper/constants/helper.enum.constant'; +} from 'src/common/helper/enums/helper.enum'; import { IHelperDateService } from 'src/common/helper/interfaces/helper.date-service.interface'; import { IHelperDateSetTimeOptions, diff --git a/src/common/helper/services/helper.encryption.service.ts b/src/common/helper/services/helper.encryption.service.ts index 539ed869d..5036ed084 100644 --- a/src/common/helper/services/helper.encryption.service.ts +++ b/src/common/helper/services/helper.encryption.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { AES, enc, mode, pad } from 'crypto-js'; import { IHelperEncryptionService } from 'src/common/helper/interfaces/helper.encryption-service.interface'; @@ -9,7 +10,15 @@ import { @Injectable() export class HelperEncryptionService implements IHelperEncryptionService { - constructor(private readonly jwtService: JwtService) {} + private readonly debug: boolean; + private readonly logger = new Logger(HelperEncryptionService.name); + + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService + ) { + this.debug = this.configService.get('app.debug'); + } base64Encrypt(data: string): string { const buff: Buffer = Buffer.from(data, 'utf8'); @@ -89,6 +98,10 @@ export class HelperEncryptionService implements IHelperEncryptionService { return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } diff --git a/src/common/helper/services/helper.string.service.ts b/src/common/helper/services/helper.string.service.ts index a008a2668..b12c3c53f 100644 --- a/src/common/helper/services/helper.string.service.ts +++ b/src/common/helper/services/helper.string.service.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { IHelperStringCurrencyOptions, IHelperStringPasswordOptions, - IHelperStringRandomOptions, } from 'src/common/helper/interfaces/helper.interface'; import { IHelperStringService } from 'src/common/helper/interfaces/helper.string-service.interface'; @@ -11,21 +10,15 @@ import { IHelperStringService } from 'src/common/helper/interfaces/helper.string export class HelperStringService implements IHelperStringService { randomReference(length: number): string { const timestamp = `${new Date().getTime()}`; - const randomString: string = this.random(length, { - safe: true, - }); + const randomString: string = this.random(length); return `${timestamp}${randomString}`.toUpperCase(); } - random(length: number, options?: IHelperStringRandomOptions): string { - return options?.safe - ? faker.string.alphanumeric({ - length: { min: length, max: length }, - }) - : faker.string.numeric({ - length: { min: length, max: length }, - }); + random(length: number): string { + return faker.string.alphanumeric({ + length: { min: length, max: length }, + }); } censor(text: string): string { diff --git a/src/common/message/constants/message.enum.constant.ts b/src/common/message/enums/message.enum.ts similarity index 77% rename from src/common/message/constants/message.enum.constant.ts rename to src/common/message/enums/message.enum.ts index f4e18bc89..be9490827 100644 --- a/src/common/message/constants/message.enum.constant.ts +++ b/src/common/message/enums/message.enum.ts @@ -1,4 +1,3 @@ export enum ENUM_MESSAGE_LANGUAGE { EN = 'en', - ID = 'id', } diff --git a/src/common/message/interfaces/message.interface.ts b/src/common/message/interfaces/message.interface.ts index 3d386cfed..a91a2e6f8 100644 --- a/src/common/message/interfaces/message.interface.ts +++ b/src/common/message/interfaces/message.interface.ts @@ -16,9 +16,9 @@ export interface IMessageValidationError { } export interface IMessageValidationImportErrorParam { - sheetName: string; + sheetName?: string; row: number; - error: ValidationError[]; + errors: ValidationError[]; } export interface IMessageValidationImportError diff --git a/src/common/message/interfaces/message.service.interface.ts b/src/common/message/interfaces/message.service.interface.ts index 6eae2c524..2052e8c21 100644 --- a/src/common/message/interfaces/message.service.interface.ts +++ b/src/common/message/interfaces/message.service.interface.ts @@ -1,5 +1,5 @@ import { ValidationError } from '@nestjs/common'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; import { IMessageErrorOptions, IMessageSetOptions, diff --git a/src/common/message/message.module.ts b/src/common/message/message.module.ts index f53251d74..f4e792e0f 100644 --- a/src/common/message/message.module.ts +++ b/src/common/message/message.module.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { I18nModule, HeaderResolver, I18nJsonLoader } from 'nestjs-i18n'; import { ConfigService } from '@nestjs/config'; import { MessageService } from 'src/common/message/services/message.service'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; @Global() @Module({}) @@ -15,6 +15,9 @@ export class MessageModule { exports: [MessageService], imports: [ I18nModule.forRootAsync({ + loader: I18nJsonLoader, + inject: [ConfigService], + resolvers: [new HeaderResolver(['x-custom-lang'])], useFactory: (configService: ConfigService) => ({ fallbackLanguage: configService .get('message.availableLanguage') @@ -28,9 +31,6 @@ export class MessageModule { watch: true, }, }), - loader: I18nJsonLoader, - inject: [ConfigService], - resolvers: [new HeaderResolver(['x-custom-lang'])], }), ], controllers: [], diff --git a/src/common/message/services/message.service.ts b/src/common/message/services/message.service.ts index b1fc8d785..488f81320 100644 --- a/src/common/message/services/message.service.ts +++ b/src/common/message/services/message.service.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { ValidationError } from 'class-validator'; import { I18nService } from 'nestjs-i18n'; import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; import { IMessageErrorOptions, IMessageSetOptions, @@ -67,9 +67,24 @@ export class MessageService implements IMessageService { ): IMessageValidationError[] { const messages: IMessageValidationError[] = []; for (const error of errors) { - const property = error.property ?? 'unknown'; + const property = error.property; const constraints: string[] = Object.keys(error.constraints ?? []); + if (constraints.length === 0) { + messages.push({ + property, + message: this.setMessage('request.unknownMessage', { + customLanguage: options?.customLanguage, + properties: { + property, + value: error.value, + }, + }), + }); + + continue; + } + for (const constraint of constraints) { const message = this.setMessage(`request.${constraint}`, { customLanguage: options?.customLanguage, @@ -96,7 +111,7 @@ export class MessageService implements IMessageService { return errors.map(val => ({ row: val.row, sheetName: val.sheetName, - errors: this.setValidationMessage(val.error, options), + errors: this.setValidationMessage(val.errors, options), })); } } diff --git a/src/common/pagination/constants/pagination.constant.ts b/src/common/pagination/constants/pagination.constant.ts index e60686e9e..1ad45f355 100644 --- a/src/common/pagination/constants/pagination.constant.ts +++ b/src/common/pagination/constants/pagination.constant.ts @@ -1,4 +1,4 @@ -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; //! Pagination default variable export const PAGINATION_DEFAULT_PER_PAGE = 20; diff --git a/src/common/pagination/dtos/pagination.list.dto.ts b/src/common/pagination/dtos/pagination.list.dto.ts index 903d2c16d..8e720d647 100644 --- a/src/common/pagination/dtos/pagination.list.dto.ts +++ b/src/common/pagination/dtos/pagination.list.dto.ts @@ -1,6 +1,6 @@ import { ApiHideProperty } from '@nestjs/swagger'; import { IsOptional } from 'class-validator'; -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; export class PaginationListDto { diff --git a/src/common/pagination/constants/pagination.enum.constant.ts b/src/common/pagination/enums/pagination.enum.ts similarity index 100% rename from src/common/pagination/constants/pagination.enum.constant.ts rename to src/common/pagination/enums/pagination.enum.ts diff --git a/src/common/pagination/interfaces/pagination.interface.ts b/src/common/pagination/interfaces/pagination.interface.ts index 124563f4b..d6bca7c50 100644 --- a/src/common/pagination/interfaces/pagination.interface.ts +++ b/src/common/pagination/interfaces/pagination.interface.ts @@ -1,7 +1,7 @@ import { ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS, ENUM_PAGINATION_ORDER_DIRECTION_TYPE, -} from 'src/common/pagination/constants/pagination.enum.constant'; +} from 'src/common/pagination/enums/pagination.enum'; export type IPaginationOrder = Record< string, diff --git a/src/common/pagination/interfaces/pagination.service.interface.ts b/src/common/pagination/interfaces/pagination.service.interface.ts index adf580b8a..9f7ede9bb 100644 --- a/src/common/pagination/interfaces/pagination.service.interface.ts +++ b/src/common/pagination/interfaces/pagination.service.interface.ts @@ -1,4 +1,4 @@ -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; export interface IPaginationService { diff --git a/src/common/pagination/pagination.module.ts b/src/common/pagination/pagination.module.ts index f83d41213..b707357fd 100644 --- a/src/common/pagination/pagination.module.ts +++ b/src/common/pagination/pagination.module.ts @@ -1,9 +1,16 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Global, Module } from '@nestjs/common'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; -@Module({ - providers: [PaginationService], - exports: [PaginationService], - imports: [], -}) -export class PaginationModule {} +@Global() +@Module({}) +export class PaginationModule { + static forRoot(): DynamicModule { + return { + module: PaginationModule, + providers: [PaginationService], + exports: [PaginationService], + imports: [], + controllers: [], + }; + } +} diff --git a/src/common/pagination/pipes/pagination.filter-date.pipe.ts b/src/common/pagination/pipes/pagination.filter-date.pipe.ts index 3bb7687b8..d56b14c20 100644 --- a/src/common/pagination/pipes/pagination.filter-date.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-date.pipe.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, mixin, Type } from '@nestjs/common'; import { PipeTransform, Scope } from '@nestjs/common/interfaces'; import { REQUEST } from '@nestjs/core'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS } from 'src/common/pagination/enums/pagination.enum'; import { IPaginationFilterDateOptions } from 'src/common/pagination/interfaces/pagination.interface'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @@ -21,7 +21,7 @@ export function PaginationFilterDatePipe( async transform(value: string): Promise> { if (!value) { - return undefined; + return; } if (options?.raw) { diff --git a/src/common/pagination/pipes/pagination.filter-equal.pipe.ts b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts index 87fcfbf05..e06045ab0 100644 --- a/src/common/pagination/pipes/pagination.filter-equal.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts @@ -20,7 +20,7 @@ export function PaginationFilterEqualPipe( value: string ): Promise> { if (!value) { - return undefined; + return; } if (options?.raw) { diff --git a/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts index 5567fb899..cecbcfe6b 100644 --- a/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts @@ -33,15 +33,8 @@ export function PaginationFilterInBooleanPipe( ) : defaultValue; - if (finalValue.length === 2) { - return undefined; - } - this.addToRequestInstance(finalValue); - return this.paginationService.filterEqual( - field, - finalValue[0] - ); + return this.paginationService.filterIn(field, finalValue); } addToRequestInstance(value: any): void { diff --git a/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts index 4982b7cd3..4e360d260 100644 --- a/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts @@ -1,6 +1,7 @@ import { Inject, Injectable, mixin, Type } from '@nestjs/common'; import { PipeTransform, Scope } from '@nestjs/common/interfaces'; import { REQUEST } from '@nestjs/core'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; import { IPaginationFilterOptions } from 'src/common/pagination/interfaces/pagination.interface'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @@ -15,7 +16,8 @@ export function PaginationFilterInEnumPipe( class MixinPaginationFilterInEnumPipe implements PipeTransform { constructor( @Inject(REQUEST) protected readonly request: IRequestApp, - private readonly paginationService: PaginationService + private readonly paginationService: PaginationService, + private readonly helperArrayService: HelperArrayService ) {} async transform(value: string): Promise> { @@ -27,10 +29,10 @@ export function PaginationFilterInEnumPipe( } const finalValue: T[] = value - ? (value - .split(',') - .map((val: string) => defaultEnum[val]) - .filter((val: string) => val) as T[]) + ? this.helperArrayService.getIntersection( + value.split(',') as T[], + Object.values(defaultEnum) as T[] + ) : (defaultValue as T[]); return this.paginationService.filterIn(field, finalValue); diff --git a/src/common/pagination/pipes/pagination.filter-nin-enum.pipe.ts b/src/common/pagination/pipes/pagination.filter-nin-enum.pipe.ts index 334e3b732..c89c50d33 100644 --- a/src/common/pagination/pipes/pagination.filter-nin-enum.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-nin-enum.pipe.ts @@ -1,6 +1,7 @@ import { Inject, Injectable, mixin, Type } from '@nestjs/common'; import { PipeTransform, Scope } from '@nestjs/common/interfaces'; import { REQUEST } from '@nestjs/core'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; import { IPaginationFilterOptions } from 'src/common/pagination/interfaces/pagination.interface'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @@ -15,7 +16,8 @@ export function PaginationFilterNinEnumPipe( class MixinPaginationFilterInEnumPipe implements PipeTransform { constructor( @Inject(REQUEST) protected readonly request: IRequestApp, - private readonly paginationService: PaginationService + private readonly paginationService: PaginationService, + private readonly helperArrayService: HelperArrayService ) {} async transform(value: string): Promise> { @@ -27,10 +29,10 @@ export function PaginationFilterNinEnumPipe( } const finalValue: T[] = value - ? (value - .split(',') - .map((val: string) => defaultEnum[val]) - .filter((val: string) => val) as T[]) + ? this.helperArrayService.getIntersection( + value.split(',') as T[], + Object.values(defaultEnum) as T[] + ) : (defaultValue as T[]); return this.paginationService.filterNin(field, finalValue); diff --git a/src/common/pagination/pipes/pagination.filter-not-equal.pipe.ts b/src/common/pagination/pipes/pagination.filter-not-equal.pipe.ts index fab7c79b9..f8cb5db6e 100644 --- a/src/common/pagination/pipes/pagination.filter-not-equal.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-not-equal.pipe.ts @@ -20,7 +20,7 @@ export function PaginationFilterNotEqualPipe( value: string ): Promise> { if (!value) { - return undefined; + return; } if (options?.raw) { @@ -35,7 +35,7 @@ export function PaginationFilterNotEqualPipe( : value.trim(); this.addToRequestInstance(finalValue); - return this.paginationService.filterEqual(field, finalValue); + return this.paginationService.filterNotEqual(field, finalValue); } addToRequestInstance(value: any): void { diff --git a/src/common/pagination/pipes/pagination.filter-string-contain.pipe.ts b/src/common/pagination/pipes/pagination.filter-string-contain.pipe.ts index 3d6d1fb82..66c2ea46e 100644 --- a/src/common/pagination/pipes/pagination.filter-string-contain.pipe.ts +++ b/src/common/pagination/pipes/pagination.filter-string-contain.pipe.ts @@ -18,7 +18,7 @@ export function PaginationFilterStringContainPipe( async transform(value: string): Promise> { if (!value) { - return undefined; + return; } if (options?.raw) { diff --git a/src/common/pagination/pipes/pagination.order.pipe.ts b/src/common/pagination/pipes/pagination.order.pipe.ts index 4b3252d6a..feb9a5481 100644 --- a/src/common/pagination/pipes/pagination.order.pipe.ts +++ b/src/common/pagination/pipes/pagination.order.pipe.ts @@ -7,7 +7,7 @@ import { PAGINATION_DEFAULT_ORDER_BY, PAGINATION_DEFAULT_ORDER_DIRECTION, } from 'src/common/pagination/constants/pagination.constant'; -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; diff --git a/src/common/pagination/pipes/pagination.search.pipe.ts b/src/common/pagination/pipes/pagination.search.pipe.ts index 42dbae588..325d8d3a3 100644 --- a/src/common/pagination/pipes/pagination.search.pipe.ts +++ b/src/common/pagination/pipes/pagination.search.pipe.ts @@ -19,7 +19,7 @@ export function PaginationSearchPipe( ): Promise> { if (availableSearch.length === 0 || !value?.search) { this.addToRequestInstance(value?.search, availableSearch); - return undefined; + return value; } const search: Record = this.paginationService.search( diff --git a/src/common/pagination/services/pagination.service.ts b/src/common/pagination/services/pagination.service.ts index d7756906f..218da2713 100644 --- a/src/common/pagination/services/pagination.service.ts +++ b/src/common/pagination/services/pagination.service.ts @@ -76,11 +76,11 @@ export class PaginationService implements IPaginationService { availableSearch: string[] ): Record { if ( - searchValue === undefined || + !searchValue || searchValue === '' || availableSearch.length === 0 ) { - return undefined; + return; } return DatabaseQueryOr( diff --git a/src/common/policy/constants/policy.status-code.constant.ts b/src/common/policy/constants/policy.status-code.constant.ts deleted file mode 100644 index 4645a58bf..000000000 --- a/src/common/policy/constants/policy.status-code.constant.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ENUM_POLICY_STATUS_CODE_ERROR { - ABILITY_FORBIDDEN_ERROR = 5010, - ROLE_FORBIDDEN_ERROR = 5011, -} diff --git a/src/common/request/constants/request.constant.ts b/src/common/request/constants/request.constant.ts index 858b5e0d9..3689d2793 100644 --- a/src/common/request/constants/request.constant.ts +++ b/src/common/request/constants/request.constant.ts @@ -1,4 +1,3 @@ -export const REQUEST_CUSTOM_TIMEOUT_META_KEY = - 'CommonRequestCustomTimeoutMetaKey'; +export const REQUEST_CUSTOM_TIMEOUT_META_KEY = 'RequestCustomTimeoutMetaKey'; export const REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY = - 'CommonRequestCustomTimeoutValueMetaKey'; + 'RequestCustomTimeoutValueMetaKey'; diff --git a/src/common/request/constants/request.status-code.constant.ts b/src/common/request/constants/request.status-code.constant.ts deleted file mode 100644 index 13b22716c..000000000 --- a/src/common/request/constants/request.status-code.constant.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ENUM_REQUEST_STATUS_CODE_ERROR { - VALIDATION_ERROR = 5030, - TIMEOUT_ERROR = 5031, -} diff --git a/src/common/request/decorators/request.decorator.ts b/src/common/request/decorators/request.decorator.ts index a34341772..61a89600e 100644 --- a/src/common/request/decorators/request.decorator.ts +++ b/src/common/request/decorators/request.decorator.ts @@ -1,24 +1,10 @@ -import { - applyDecorators, - createParamDecorator, - ExecutionContext, - SetMetadata, -} from '@nestjs/common'; +import { applyDecorators, SetMetadata } from '@nestjs/common'; import { REQUEST_CUSTOM_TIMEOUT_META_KEY, REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, } from 'src/common/request/constants/request.constant'; -import { IRequestApp } from 'src/common/request/interfaces/request.interface'; - -//! Get request language -export const RequestLanguage: () => ParameterDecorator = createParamDecorator( - (_: unknown, ctx: ExecutionContext): string => { - const { __language } = ctx.switchToHttp().getRequest(); - return __language; - } -); -//! custom request timeout +//! custom app timeout export function RequestTimeout(seconds: string): MethodDecorator { return applyDecorators( SetMetadata(REQUEST_CUSTOM_TIMEOUT_META_KEY, true), diff --git a/src/common/request/enums/request.status-code.enum.ts b/src/common/request/enums/request.status-code.enum.ts new file mode 100644 index 000000000..0a9368218 --- /dev/null +++ b/src/common/request/enums/request.status-code.enum.ts @@ -0,0 +1,4 @@ +export enum ENUM_REQUEST_STATUS_CODE_ERROR { + VALIDATION = 5030, + TIMEOUT = 5031, +} diff --git a/src/common/request/exceptions/request.validation.exception.ts b/src/common/request/exceptions/request.validation.exception.ts index 59977bb61..beb70a328 100644 --- a/src/common/request/exceptions/request.validation.exception.ts +++ b/src/common/request/exceptions/request.validation.exception.ts @@ -1,16 +1,15 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; +import { HttpStatus } from '@nestjs/common'; import { ValidationError } from 'class-validator'; -import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; + +export class RequestValidationException extends Error { + readonly httpStatus: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY; + readonly statusCode: number = ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION; + readonly errors: ValidationError[]; -export class RequestValidationException extends HttpException { constructor(errors: ValidationError[]) { - super( - { - statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION_ERROR, - message: 'request.validation', - errors, - }, - HttpStatus.UNPROCESSABLE_ENTITY - ); + super('request.validation'); + + this.errors = errors; } } diff --git a/src/common/request/interceptors/request.timeout.interceptor.ts b/src/common/request/interceptors/request.timeout.interceptor.ts index 4b0c641d2..69e68f1b5 100644 --- a/src/common/request/interceptors/request.timeout.interceptor.ts +++ b/src/common/request/interceptors/request.timeout.interceptor.ts @@ -14,7 +14,7 @@ import { REQUEST_CUSTOM_TIMEOUT_META_KEY, REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, } from 'src/common/request/constants/request.constant'; -import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; @Injectable() export class RequestTimeoutInterceptor @@ -49,7 +49,7 @@ export class RequestTimeoutInterceptor if (err instanceof TimeoutError) { throw new RequestTimeoutException({ statusCode: - ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT_ERROR, + ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, message: 'http.clientError.requestTimeOut', }); } @@ -63,7 +63,7 @@ export class RequestTimeoutInterceptor if (err instanceof TimeoutError) { throw new RequestTimeoutException({ statusCode: - ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT_ERROR, + ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, message: 'http.clientError.requestTimeOut', }); } diff --git a/src/common/request/interfaces/request.interface.ts b/src/common/request/interfaces/request.interface.ts index 988f61a22..34bcccfa5 100644 --- a/src/common/request/interfaces/request.interface.ts +++ b/src/common/request/interfaces/request.interface.ts @@ -1,14 +1,12 @@ import { Request } from 'express'; +import { ApiKeyPayloadDto } from 'src/modules/api-key/dtos/api-key.payload.dto'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; import { ResponsePagingMetadataPaginationDto } from 'src/common/response/dtos/response.paging.dto'; -export interface IRequestApp< - T = Record, - N = Record, - B = Record, -> extends Request { +export interface IRequestApp + extends Request { apiKey?: B; user?: T; - __user?: N; __language: string; __version: string; diff --git a/src/common/request/pipes/request.parse-plain-object.pipe.ts b/src/common/request/pipes/request.parse-plain-object.pipe.ts deleted file mode 100644 index f418360ba..000000000 --- a/src/common/request/pipes/request.parse-plain-object.pipe.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PipeTransform, Injectable } from '@nestjs/common'; -import { Document } from 'mongoose'; -import { DatabaseEntityAbstract } from 'src/common/database/abstracts/base/database.entity.abstract'; - -@Injectable() -export class RequestParsePlainObjectPipe< - T extends Document, - N extends DatabaseEntityAbstract, -> implements PipeTransform -{ - async transform(value: T): Promise { - return value.toObject(); - } -} diff --git a/src/common/request/request.module.ts b/src/common/request/request.module.ts index e2de4af68..89f39f385 100644 --- a/src/common/request/request.module.ts +++ b/src/common/request/request.module.ts @@ -5,9 +5,9 @@ import { ValidationPipe, } from '@nestjs/common'; import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { RequestTimeoutInterceptor } from 'src/common/request/interceptors/request.timeout.interceptor'; import { ValidationError } from 'class-validator'; import { RequestValidationException } from 'src/common/request/exceptions/request.validation.exception'; +import { RequestTimeoutInterceptor } from 'src/common/request/interceptors/request.timeout.interceptor'; import { DateGreaterThanConstraint, DateGreaterThanEqualConstraint, diff --git a/src/common/response/constants/response.constant.ts b/src/common/response/constants/response.constant.ts index 619424596..ff3a5dae0 100644 --- a/src/common/response/constants/response.constant.ts +++ b/src/common/response/constants/response.constant.ts @@ -1,7 +1,4 @@ export const RESPONSE_MESSAGE_PROPERTIES_META_KEY = 'ResponseMessagePropertiesMetaKey'; export const RESPONSE_MESSAGE_PATH_META_KEY = 'ResponseMessagePathMetaKey'; - export const RESPONSE_FILE_EXCEL_TYPE_META_KEY = 'ResponseFileExcelTypeMetaKey'; -export const RESPONSE_FILE_EXCEL_PASSWORD_META_KEY = - 'ResponseFileExcelPasswordMetaKey'; diff --git a/src/common/response/decorators/response.decorator.ts b/src/common/response/decorators/response.decorator.ts index 9f0d3c733..4ce0f2da0 100644 --- a/src/common/response/decorators/response.decorator.ts +++ b/src/common/response/decorators/response.decorator.ts @@ -1,7 +1,5 @@ import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common'; -import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/constants/helper.enum.constant'; import { - RESPONSE_FILE_EXCEL_PASSWORD_META_KEY, RESPONSE_FILE_EXCEL_TYPE_META_KEY, RESPONSE_MESSAGE_PATH_META_KEY, RESPONSE_MESSAGE_PROPERTIES_META_KEY, @@ -13,44 +11,77 @@ import { IResponseOptions, IResponseFileExcelOptions, } from 'src/common/response/interfaces/response.interface'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; export function Response( messagePath: string, options?: IResponseOptions ): MethodDecorator { - return applyDecorators( + const decorators: any = [ UseInterceptors(ResponseInterceptor), SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), SetMetadata( RESPONSE_MESSAGE_PROPERTIES_META_KEY, options?.messageProperties - ) - ); -} - -export function ResponseFileExcel( - options?: IResponseFileExcelOptions -): MethodDecorator { - return applyDecorators( - UseInterceptors(ResponseFileExcelInterceptor), - SetMetadata( - RESPONSE_FILE_EXCEL_TYPE_META_KEY, - options?.type ?? ENUM_HELPER_FILE_EXCEL_TYPE.CSV ), - SetMetadata(RESPONSE_FILE_EXCEL_PASSWORD_META_KEY, options?.password) - ); + ]; + + if (options?.cached) { + decorators.push(UseInterceptors(CacheInterceptor)); + + if (typeof options?.cached !== 'boolean') { + if (options?.cached?.key) { + decorators.push(CacheKey(options?.cached?.key)); + } + + if (options?.cached?.ttl) { + decorators.push(CacheTTL(options?.cached?.ttl)); + } + } + } + + return applyDecorators(...decorators); } export function ResponsePaging( messagePath: string, options?: IResponseOptions ): MethodDecorator { - return applyDecorators( + const decorators: any = [ UseInterceptors(ResponsePagingInterceptor), SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), SetMetadata( RESPONSE_MESSAGE_PROPERTIES_META_KEY, options?.messageProperties + ), + ]; + + if (options?.cached) { + decorators.push(UseInterceptors(CacheInterceptor)); + + if (typeof options?.cached !== 'boolean') { + if (options?.cached?.key) { + decorators.push(CacheKey(options?.cached?.key)); + } + + if (options?.cached?.ttl) { + decorators.push(CacheTTL(options?.cached?.ttl)); + } + } + } + + return applyDecorators(...decorators); +} + +export function ResponseFileExcel( + options?: IResponseFileExcelOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponseFileExcelInterceptor), + SetMetadata( + RESPONSE_FILE_EXCEL_TYPE_META_KEY, + options?.type ?? ENUM_HELPER_FILE_EXCEL_TYPE.CSV ) ); } diff --git a/src/common/response/dtos/response.dto.ts b/src/common/response/dtos/response.dto.ts index f5931ea45..cc61c2775 100644 --- a/src/common/response/dtos/response.dto.ts +++ b/src/common/response/dtos/response.dto.ts @@ -36,7 +36,7 @@ export class ResponseDto { required: true, nullable: false, description: 'Contain metadata about API', - type: () => ResponseMetadataDto, + type: ResponseMetadataDto, example: { language: 'en', timestamp: 1660190937231, diff --git a/src/common/response/dtos/response.paging.dto.ts b/src/common/response/dtos/response.paging.dto.ts index 952cc8f38..978a3b857 100644 --- a/src/common/response/dtos/response.paging.dto.ts +++ b/src/common/response/dtos/response.paging.dto.ts @@ -1,34 +1,12 @@ import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty, PickType } from '@nestjs/swagger'; +import { ApiProperty, PickType } from '@nestjs/swagger'; import { PAGINATION_DEFAULT_AVAILABLE_ORDER_DIRECTION } from 'src/common/pagination/constants/pagination.constant'; -import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; import { ResponseDto, ResponseMetadataDto, } from 'src/common/response/dtos/response.dto'; -export class ResponsePagingMetadataCursorDto { - @ApiProperty({ - required: true, - }) - nextPage: string; - - @ApiProperty({ - required: true, - }) - previousPage: string; - - @ApiProperty({ - required: true, - }) - firstPage: string; - - @ApiProperty({ - required: true, - }) - lastPage: string; -} - export class ResponsePagingMetadataRequestDto { @ApiProperty({ required: true, @@ -112,13 +90,7 @@ export class ResponsePagingMetadataPaginationDto extends ResponsePagingMetadataR export class ResponsePagingMetadataDto extends ResponseMetadataDto { @ApiProperty({ required: false, - type: () => ResponsePagingMetadataCursorDto, - }) - cursor?: ResponsePagingMetadataCursorDto; - - @ApiProperty({ - required: false, - type: () => ResponsePagingMetadataPaginationDto, + type: ResponsePagingMetadataPaginationDto, }) pagination?: ResponsePagingMetadataPaginationDto; } @@ -132,7 +104,7 @@ export class ResponsePagingDto extends PickType(ResponseDto, [ required: true, nullable: false, description: 'Contain metadata about API', - type: () => ResponsePagingMetadataDto, + type: ResponsePagingMetadataDto, example: { language: 'en', timestamp: 1660190937231, @@ -142,6 +114,7 @@ export class ResponsePagingDto extends PickType(ResponseDto, [ repoVersion: '1.0.0', pagination: { search: faker.person.fullName(), + filters: {}, page: 1, perPage: 20, orderBy: 'createdAt', @@ -153,16 +126,14 @@ export class ResponsePagingDto extends PickType(ResponseDto, [ total: 100, totalPage: 5, }, - cursor: { - nextPage: `http://217.0.0.1/__path?perPage=10&page=3&search=abc`, - previousPage: `http://217.0.0.1/__path?perPage=10&page=1&search=abc`, - firstPage: `http://217.0.0.1/__path?perPage=10&page=1&search=abc`, - lastPage: `http://217.0.0.1/__path?perPage=10&page=20&search=abc`, - }, }, }) - readonly _metadata: ResponsePagingMetadataDto; + _metadata: ResponsePagingMetadataDto; - @ApiHideProperty() - data?: Record[]; + @ApiProperty({ + required: true, + isArray: true, + default: [], + }) + data: Record[]; } diff --git a/src/common/response/interceptors/response.file.interceptor.ts b/src/common/response/interceptors/response.file.interceptor.ts index b2c036744..1cccdd49e 100644 --- a/src/common/response/interceptors/response.file.interceptor.ts +++ b/src/common/response/interceptors/response.file.interceptor.ts @@ -10,15 +10,12 @@ import { map } from 'rxjs/operators'; import { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { Response } from 'express'; import { Reflector } from '@nestjs/core'; -import { - RESPONSE_FILE_EXCEL_PASSWORD_META_KEY, - RESPONSE_FILE_EXCEL_TYPE_META_KEY, -} from 'src/common/response/constants/response.constant'; +import { RESPONSE_FILE_EXCEL_TYPE_META_KEY } from 'src/common/response/constants/response.constant'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/constants/helper.enum.constant'; -import { ENUM_FILE_MIME } from 'src/common/file/constants/file.enum.constant'; import { IResponseFileExcel } from 'src/common/response/interfaces/response.interface'; import { FileService } from 'src/common/file/services/file.service'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; @Injectable() export class ResponseFileExcelInterceptor @@ -46,11 +43,6 @@ export class ResponseFileExcelInterceptor context.getHandler() ); - const password: string = this.reflector.get( - RESPONSE_FILE_EXCEL_PASSWORD_META_KEY, - context.getHandler() - ); - // set default response const responseData = (await res) as IResponseFileExcel; @@ -68,10 +60,7 @@ export class ResponseFileExcelInterceptor if (type === ENUM_HELPER_FILE_EXCEL_TYPE.XLSX) { // create file const file: Buffer = this.fileService.writeExcel( - responseData.data, - { - password, - } + responseData.data ); // set headers diff --git a/src/common/response/interceptors/response.paging.interceptor.ts b/src/common/response/interceptors/response.paging.interceptor.ts index 5bd6452f8..7d7477249 100644 --- a/src/common/response/interceptors/response.paging.interceptor.ts +++ b/src/common/response/interceptors/response.paging.interceptor.ts @@ -11,7 +11,6 @@ import { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { Response } from 'express'; import { MessageService } from 'src/common/message/services/message.service'; import { Reflector } from '@nestjs/core'; -import qs from 'qs'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; import { IMessageOptionsProperties } from 'src/common/message/interfaces/message.interface'; import { @@ -19,10 +18,8 @@ import { RESPONSE_MESSAGE_PROPERTIES_META_KEY, } from 'src/common/response/constants/response.constant'; import { IResponsePaging } from 'src/common/response/interfaces/response.interface'; -import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; import { ResponsePagingDto, - ResponsePagingMetadataCursorDto, ResponsePagingMetadataDto, } from 'src/common/response/dtos/response.paging.dto'; import { ConfigService } from '@nestjs/config'; @@ -35,7 +32,6 @@ export class ResponsePagingInterceptor constructor( private readonly reflector: Reflector, private readonly messageService: MessageService, - private readonly helperArrayService: HelperArrayService, private readonly configService: ConfigService, private readonly helperDateService: HelperDateService ) {} @@ -120,72 +116,15 @@ export class ResponsePagingInterceptor delete _metadata?.customProperty; // metadata pagination - const { query } = request; - delete query.perPage; - delete query.page; - - const total: number = responseData._pagination.total; - const totalPage: number = - responseData._pagination.totalPage; - const perPage: number = xPagination.perPage; - const page: number = xPagination.page; - - const queryString = qs.stringify(query, { - encode: false, - }); - - const cursorPaginationMetadata: ResponsePagingMetadataCursorDto = - { - nextPage: - page < totalPage - ? queryString - ? `${xPath}?perPage=${perPage}&page=${ - page + 1 - }&${queryString}` - : `${xPath}?perPage=${perPage}&page=${page + 1}` - : undefined, - previousPage: - page > 1 - ? queryString - ? `${xPath}?perPage=${perPage}&page=${ - page - 1 - }&${queryString}` - : `${xPath}?perPage=${perPage}&page=${page - 1}` - : undefined, - firstPage: - totalPage > 1 - ? queryString - ? `${xPath}?perPage=${perPage}&page=${1}&${queryString}` - : `${xPath}?perPage=${perPage}&page=${1}` - : undefined, - lastPage: - totalPage > 1 - ? queryString - ? `${xPath}?perPage=${perPage}&page=${totalPage}&${queryString}` - : `${xPath}?perPage=${perPage}&page=${totalPage}` - : undefined, - }; - metadata = { ...metadata, ..._metadata, pagination: { ...xPagination, - ...metadata._pagination, - total, - totalPage: data.length > 0 ? totalPage : 0, + ...responseData._pagination, }, }; - if ( - !this.helperArrayService.notIn( - Object.values(cursorPaginationMetadata), - undefined - ) - ) { - metadata.cursor = cursorPaginationMetadata; - } - const message: string = this.messageService.setMessage( messagePath, { diff --git a/src/common/response/interfaces/response.interface.ts b/src/common/response/interfaces/response.interface.ts index 5ca098ffd..c77e644a3 100644 --- a/src/common/response/interfaces/response.interface.ts +++ b/src/common/response/interfaces/response.interface.ts @@ -1,6 +1,6 @@ import { HttpStatus } from '@nestjs/common'; import { IFileRows } from 'src/common/file/interfaces/file.interface'; -import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; import { IMessageOptionsProperties } from 'src/common/message/interfaces/message.interface'; export interface IResponseCustomProperty { @@ -19,11 +19,11 @@ export interface IResponseMetadata { // decorator options export interface IResponseOptions { messageProperties?: IMessageOptionsProperties; + cached?: IResponseCacheOptions | boolean; } export interface IResponseFileExcelOptions { type?: ENUM_HELPER_FILE_EXCEL_TYPE; - password?: string; } // response @@ -47,3 +47,9 @@ export interface IResponsePaging { export interface IResponseFileExcel { data: IFileRows[]; } + +// cached +export interface IResponseCacheOptions { + key?: string; + ttl?: number; +} diff --git a/src/configs/app.config.ts b/src/configs/app.config.ts index 62830d383..f92ee2b67 100644 --- a/src/configs/app.config.ts +++ b/src/configs/app.config.ts @@ -1,37 +1,32 @@ import { registerAs } from '@nestjs/config'; import { version } from 'package.json'; -import { - ENUM_APP_ENVIRONMENT, - ENUM_APP_TIMEZONE, -} from 'src/app/constants/app.enum.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; export default registerAs( 'app', (): Record => ({ - name: process.env.APP_NAME ?? 'ack', - env: process.env.APP_ENV ?? ENUM_APP_ENVIRONMENT.DEVELOPMENT, - timezone: process.env.APP_TIMEZONE ?? ENUM_APP_TIMEZONE.ASIA_SINGAPORE, + name: process.env.APP_NAME, + env: process.env.APP_ENV, + timezone: process.env.APP_TIMEZONE, repoVersion: version, globalPrefix: process.env.APP_ENV === ENUM_APP_ENVIRONMENT.PRODUCTION ? '' : '/api', - debug: process.env.APP_DEBUG === 'true' ?? false, + debug: process.env.APP_DEBUG === 'true', - jobEnable: process.env.JOB_ENABLE === 'true' ?? false, + jobEnable: process.env.JOB_ENABLE === 'true', http: { - enable: process.env.HTTP_ENABLE === 'true' ?? false, - host: process.env.HTTP_HOST ?? 'localhost', - port: process.env.HTTP_PORT - ? Number.parseInt(process.env.HTTP_PORT) - : 3000, + enable: process.env.HTTP_ENABLE === 'true', + host: process.env.HTTP_HOST, + port: Number.parseInt(process.env.HTTP_PORT), }, urlVersion: { - enable: process.env.URL_VERSION_ENABLE === 'true' ?? false, + enable: process.env.URL_VERSION_ENABLE === 'true', prefix: 'v', - version: process.env.URL_VERSION ?? '1', + version: process.env.URL_VERSION, }, }) ); diff --git a/src/configs/auth.config.ts b/src/configs/auth.config.ts index 216762425..96e30f1f5 100644 --- a/src/configs/auth.config.ts +++ b/src/configs/auth.config.ts @@ -6,34 +6,30 @@ export default registerAs( (): Record => ({ jwt: { accessToken: { - secretKey: - process.env.AUTH_JWT_ACCESS_TOKEN_SECRET_KEY ?? '123456', - expirationTime: ms( - process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED ?? '1h' - ), // 1 hours + secretKey: process.env.AUTH_JWT_ACCESS_TOKEN_SECRET_KEY, + expirationTime: + ms(process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED) / 1000, // 1 hours }, refreshToken: { - secretKey: - process.env.AUTH_JWT_REFRESH_TOKEN_SECRET_KEY ?? - '123456000', - expirationTime: ms( - process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRED ?? '90d' - ), // 1 hours + secretKey: process.env.AUTH_JWT_REFRESH_TOKEN_SECRET_KEY, + expirationTime: + ms(process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRED) / 1000, // 1 hours }, - subject: process.env.AUTH_JWT_SUBJECT ?? 'ackDevelopment', - audience: process.env.AUTH_JWT_AUDIENCE ?? 'https://example.com', - issuer: process.env.AUTH_JWT_ISSUER ?? 'ack', + subject: process.env.AUTH_JWT_SUBJECT, + audience: process.env.AUTH_JWT_AUDIENCE, + issuer: process.env.AUTH_JWT_ISSUER, prefixAuthorization: 'Bearer', }, password: { - attempt: false, + attempt: true, maxAttempt: 5, saltLength: 8, - expiredIn: ms('182d') / 1000, // 182 days - period: ms('90d') / 1000, + expiredIn: ms('182d') / 1000, // 0.5 years + expiredInTemporary: ms('3d') / 1000, // 3 days + period: ms('90d') / 1000, // 3 months }, apple: { diff --git a/src/configs/aws.config.ts b/src/configs/aws.config.ts index 1fda423b7..34e536312 100644 --- a/src/configs/aws.config.ts +++ b/src/configs/aws.config.ts @@ -11,6 +11,7 @@ export default registerAs( bucket: process.env.AWS_S3_BUCKET ?? 'bucket', region: process.env.AWS_S3_REGION, baseUrl: `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com`, + presignUrlExpired: 30 * 60 * 1000, }, ses: { credential: { diff --git a/src/configs/database.config.ts b/src/configs/database.config.ts index 6f5e8328f..bd0648194 100644 --- a/src/configs/database.config.ts +++ b/src/configs/database.config.ts @@ -9,9 +9,9 @@ export default registerAs( debug: process.env.DATABASE_DEBUG === 'true', timeoutOptions: { - serverSelectionTimeoutMS: 10000, - socketTimeoutMS: 10000, - heartbeatFrequencyMS: 30000, + serverSelectionTimeoutMS: 30 * 1000, // 30 secs + socketTimeoutMS: 30 * 1000, // 30 secs + heartbeatFrequencyMS: 30 * 1000, // 30 secs }, }) ); diff --git a/src/configs/email.config.ts b/src/configs/email.config.ts index a81269e39..e2f26d112 100644 --- a/src/configs/email.config.ts +++ b/src/configs/email.config.ts @@ -4,5 +4,8 @@ export default registerAs( 'email', (): Record => ({ fromEmail: 'noreply@mail.com', + supportEmail: 'support@mail.com', + + clientUrl: process.env.CLIENT_URL ?? 'https://example.com', }) ); diff --git a/src/configs/index.ts b/src/configs/index.ts index 13bf92642..3a9d68f55 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -9,6 +9,7 @@ import RequestConfig from 'src/configs/request.config'; import DocConfig from 'src/configs/doc.config'; import MessageConfig from 'src/configs/message.config'; import EmailConfig from 'src/configs/email.config'; +import RedisConfig from 'src/configs/redis.config'; export default [ AppConfig, @@ -22,4 +23,5 @@ export default [ DocConfig, MessageConfig, EmailConfig, + RedisConfig, ]; diff --git a/src/configs/message.config.ts b/src/configs/message.config.ts index 904ea6484..9e1411e88 100644 --- a/src/configs/message.config.ts +++ b/src/configs/message.config.ts @@ -1,10 +1,10 @@ import { registerAs } from '@nestjs/config'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; export default registerAs( 'message', (): Record => ({ availableLanguage: Object.values(ENUM_MESSAGE_LANGUAGE), - language: process.env.APP_LANGUAGE ?? ENUM_MESSAGE_LANGUAGE.EN, + language: process.env.APP_LANGUAGE, }) ); diff --git a/src/configs/redis.config.ts b/src/configs/redis.config.ts new file mode 100644 index 000000000..932c7a66c --- /dev/null +++ b/src/configs/redis.config.ts @@ -0,0 +1,15 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'redis', + (): Record => ({ + host: process.env.REDIS_HOST, + port: Number.parseInt(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, + tls: process.env.REDIS_TLS === 'true' ? {} : null, + cached: { + ttl: 5 & 1000, // 5 mins + max: 10, + }, + }) +); diff --git a/src/configs/user.config.ts b/src/configs/user.config.ts index 32d6e8370..3398fa147 100644 --- a/src/configs/user.config.ts +++ b/src/configs/user.config.ts @@ -1,9 +1,11 @@ import { registerAs } from '@nestjs/config'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; export default registerAs( 'user', (): Record => ({ + usernamePrefix: 'user', + usernamePattern: /^[a-zA-Z0-9-_]+$/, uploadPath: process.env.APP_ENV === ENUM_APP_ENVIRONMENT.PRODUCTION ? '/user/{user}' diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts deleted file mode 100644 index e722ef08a..000000000 --- a/src/jobs/jobs.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { JobsRouterModule } from 'src/jobs/router/jobs.router.module'; - -@Module({}) -export class JobsModule { - static forRoot(): DynamicModule { - const imports: ( - | DynamicModule - | Type - | Promise - | ForwardReference - )[] = []; - - if (process.env.JOB_ENABLE === 'true') { - imports.push(ScheduleModule.forRoot(), JobsRouterModule); - } - - return { - module: JobsModule, - providers: [], - exports: [], - controllers: [], - imports, - }; - } -} diff --git a/src/jobs/router/jobs.router.module.ts b/src/jobs/router/jobs.router.module.ts deleted file mode 100644 index 578b79d31..000000000 --- a/src/jobs/router/jobs.router.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; - -@Module({ - providers: [], - exports: [], - imports: [], - controllers: [], -}) -export class JobsRouterModule {} diff --git a/src/languages/en/auth.json b/src/languages/en/auth.json index ca16f51e1..4e46646db 100644 --- a/src/languages/en/auth.json +++ b/src/languages/en/auth.json @@ -1,8 +1,19 @@ { + "loginWithCredential": "User logged in successfully with credentials.", + "loginWithSocialGoogle": "User logged in successfully with Google social login.", + "loginWithSocialApple": "User logged in successfully with Apple social login.", + "refresh": "Token refreshed successfully.", + "signUp": "User signed up successfully.", + "changePassword": "User password changed successfully.", + "updatePassword": "User password updated successfully.", "error": { "accessTokenUnauthorized": "The access token is unauthorized.", "refreshTokenUnauthorized": "The refresh token is unauthorized.", "socialApple": "There was an error with Apple social login.", - "socialGoogle": "There was an error with Google social login." + "socialGoogle": "There was an error with Google social login.", + "passwordExpired": "Password has expired.", + "passwordAttemptMax": "Maximum password attempts exceeded.", + "passwordNotMatch": "Passwords do not match.", + "passwordMustNew": "New password must be different from previous passwords within {period}." } } diff --git a/src/languages/en/user.json b/src/languages/en/user.json index 31ea9aab1..3fd833118 100644 --- a/src/languages/en/user.json +++ b/src/languages/en/user.json @@ -1,37 +1,26 @@ { "list": "List of users retrieved successfully.", - "stateHistoryList": "User history retrieved successfully.", - "passwordHistoryList": "User password history retrieved successfully.", - "loginHistoryList": "User login history retrieved successfully.", "get": "User details fetched successfully.", "create": "New user created successfully.", "inactive": "User marked as inactive.", "active": "User marked as active.", "blocked": "User blocked successfully.", - "updatePassword": "User password updated successfully.", - "signUp": "User signed up successfully.", - "loginWithCredential": "User logged in successfully with credentials.", - "loginWithSocialGoogle": "User logged in successfully with Google social login.", - "loginWithSocialApple": "User logged in successfully with Apple social login.", - "refresh": "Token refreshed successfully.", - "changePassword": "User password changed successfully.", "profile": "User profile retrieved successfully.", "updateProfile": "User profile updated successfully.", "updateProfileUpload": "User profile picture uploaded successfully.", "deleteSelf": "User deleted successfully.", + "updateMobileNumber": "User update mobile number successfully.", + "updateClaimUsername": "User claim username successfully.", "error": { "mobileNumberExist": "Mobile number already exists.", "emailExist": "Email already exists.", "blocked": "User is blocked.", "deleted": "User has been deleted.", "inactive": "User is inactive.", - "passwordExpired": "Password has expired.", - "passwordAttemptMax": "Maximum password attempts exceeded.", - "passwordNotMatch": "Passwords do not match.", - "passwordMustNew": "New password must be different from previous passwords within {period}.", "mobileNumberNotAllowed": "Mobile number is not allowed.", "blockedInvalid": "Invalid blocked status.", "notFound": "User not found.", - "statusInvalid": "Invalid user status." + "statusInvalid": "Invalid user status.", + "usernameExist": "Username has been taken." } } diff --git a/src/languages/id/apiKey.json b/src/languages/id/apiKey.json deleted file mode 100644 index 828818637..000000000 --- a/src/languages/id/apiKey.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "list": "Daftar apikeys berhasil diambil.", - "get": "Detail Apikey berhasil diambil.", - "create": "Apikey baru berhasil dibuat.", - "reset": "Berhasil mengatur ulang Apikey.", - "update": "Apikey berhasil diperbarui.", - "inactive": "Apikey ditandai sebagai tidak aktif.", - "active": "Apikey ditandai sebagai aktif.", - "updateDate": "Tanggal Apikey berhasil diperbarui.", - "delete": "Apikey berhasil dihapus.", - "error": { - "xApiKey": { - "required": "xApikey dibutuhkan.", - "notFound": "xApikey tidak ditemukan.", - "inactive": "xApikey tidak aktif.", - "expired": "xApikey telah kedaluwarsa.", - "invalid": "xApikey tidak valid.", - "forbidden": "xApikey tidak memiliki izin." - }, - "expired": "Apikey telah kedaluwarsa.", - "isActiveInvalid": "Apikey status aktif tidak valid.", - "notFound": "Apikey yang diminta tidak ditemukan." - } -} diff --git a/src/languages/id/auth.json b/src/languages/id/auth.json deleted file mode 100644 index b705f49ee..000000000 --- a/src/languages/id/auth.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "error": { - "accessTokenUnauthorized": "Token akses tidak diotorisasi.", - "refreshTokenUnauthorized": "Token penyegaran tidak diotorisasi.", - "socialApple": "Ada kesalahan dengan login sosial Apple.", - "socialGoogle": "Ada kesalahan dengan login sosial Google." - } -} diff --git a/src/languages/id/country.json b/src/languages/id/country.json deleted file mode 100644 index a719a2090..000000000 --- a/src/languages/id/country.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "list": "Daftar negara berhasil diambil.", - "get": "Detail negara berhasil diambil.", - "inactive": "Negara ditandai sebagai tidak aktif.", - "active": "Negara ditandai sebagai aktif.", - "all": "Ambil semua daftar negara berhasil diambil.", - "error": { - "isActiveInvalid": "Status aktif tidak valid untuk negara tersebut.", - "notFound": "Negara yang diminta tidak ditemukan." - } -} diff --git a/src/languages/id/file.json b/src/languages/id/file.json deleted file mode 100644 index e845a2464..000000000 --- a/src/languages/id/file.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "error": { - "validationDto": "Data yang diberikan tidak memenuhi kriteria validasi.", - "mimeInvalid": "Jenis MIME dari file tidak valid.", - "requiredParseFirst": "Parsing data diperlukan sebelum operasi ini.", - "required": "Kolom ini wajib diisi dan tidak boleh dikosongkan." - } -} diff --git a/src/languages/id/health.json b/src/languages/id/health.json deleted file mode 100644 index 9d30d870a..000000000 --- a/src/languages/id/health.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "check": "Pemeriksaan kesehatan selesai. Semua sistem berfungsi normal." -} diff --git a/src/languages/id/hello.json b/src/languages/id/hello.json deleted file mode 100644 index 3d5394877..000000000 --- a/src/languages/id/hello.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hello": "Halo! Selamat datang di layanan kami." -} diff --git a/src/languages/id/http.json b/src/languages/id/http.json deleted file mode 100644 index 6c613323c..000000000 --- a/src/languages/id/http.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "success": { - "ok": "OK", - "created": "Created", - "accepted": "Accepted", - "noContent": "No Content" - }, - "redirection": { - "movePermanently": "Move Permanently", - "found": "Found", - "notModified": "Not Modified", - "temporaryRedirect": "Temporary Redirect", - "permanentRedirect": "Permanent Redirect" - }, - "clientError": { - "badRequest": "Bad Request", - "unauthorized": "Unauthorized", - "forbidden": "Forbidden", - "notFound": "Not Found", - "methodNotAllowed": "Not Allowed Method", - "notAcceptable": "Not Acceptable", - "payloadToLarge": "Payload To Large", - "uriToLarge": "Uri To Large", - "unsupportedMediaType": "Unsupported Media Type", - "unprocessableEntity": "Unprocessable Entity", - "tooManyRequest": "Too Many Request", - "requestTimeOut": "Request Timeout" - }, - "serverError": { - "internalServerError": "Internal Server Error", - "notImplemented": "Not Implemented", - "badGateway": "Bad Gateway", - "serviceUnavailable": "Service Unavailable", - "gatewayTimeout": "Gateway Timeout" - }, - - "200": "OK", - - "201": "Created", - "202": "Accepted", - "204": "No Content", - - "301": "Move Permanently", - "302": "Found", - "304": "Not Modified", - "307": "Temporary Redirect", - "308": "Permanent Redirect", - - "400": "Bad Request", - "401": "Unauthorized", - "403": "Forbidden", - "404": "Not Found", - "405": "Not Allowed Method", - "406": "Not Acceptable", - "413": "Payload To Large", - "414": "Uri To Large", - "415": "Unsupported Media Type", - "422": "Unprocessable Entity", - "429": "Too Many Request", - - "500": "Internal Server Error", - "501": "Not Implemented", - "502": "Bad Gateway", - "503": "Service Unavailable", - "504": "Gateway Timeout" -} diff --git a/src/languages/id/policy.json b/src/languages/id/policy.json deleted file mode 100644 index 42592992d..000000000 --- a/src/languages/id/policy.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "error": { - "abilityForbidden": "Anda tidak memiliki izin yang diperlukan untuk melakukan tindakan ini.", - "roleForbidden": "Peran Anda tidak memberikan akses ke sumber daya ini." - } -} diff --git a/src/languages/id/request.json b/src/languages/id/request.json deleted file mode 100644 index 7fece13f5..000000000 --- a/src/languages/id/request.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "validation": "Kesalahan validasi", - "unknownValue": "Nilai yang tidak dikenal tidak diizinkan.", - "min": "{property} memiliki elemen lebih sedikit dari yang diizinkan minimum.", - "max": "{property} memiliki elemen lebih banyak dari yang diizinkan maksimum.", - "maxLength": "{property} memiliki elemen lebih banyak dari yang diizinkan maksimum.", - "minLength": "{property} memiliki elemen lebih sedikit dari yang diizinkan minimum.", - "isString": "{property} harus berupa tipe string.", - "isNotEmpty": "{property} tidak boleh kosong.", - "isLowercase": "{property} harus dalam huruf kecil.", - "isOptional": "{property} bersifat opsional.", - "isPositive": "{property} harus menjadi angka positif.", - "isEmail": "{property} harus berupa tipe email.", - "isInt": "{property} harus berupa angka.", - "isNumberString": "{property} harus berupa angka.", - "isNumber": "{property} harus berupa angka {value}.", - "isMongoId": "{property} harus merujuk pada objek id mongo.", - "isBoolean": "{property} harus berupa boolean", - "isEnum": "{property} tidak cocok dengan enum", - "isObject": "{property} harus berupa objek", - "isArray": "{property} harus berupa array", - "arrayNotEmpty": "Array {property} tidak boleh kosong", - "minDate": "{property} memiliki tanggal lebih sedikit dari yang diizinkan minimum.", - "maxDate": "{property} memiliki elemen lebih banyak dari yang diizinkan maksimum.", - "isDate": "{property} harus berupa tanggal", - "isPasswordStrong": "{property} harus memiliki pola yang kuat", - "isPasswordMedium": "{property} harus memiliki pola yang sedang", - "isPasswordWeak": "{property} harus memiliki pola yang lemah", - "isStartWith": "{property} harus dimulai dengan {value}", - "safeString": "{property} harus string aman, hanya mengandung A-Z, a-z, 0-9 dan simbol yang diizinkan adalah '_-'", - "isOnlyDigits": "{property} harus berupa digit", - "mobileNumberAllowed": "{property} harus berupa nomor ponsel yang diizinkan", - "maxBinaryFile": "Ukuran {property} lebih dari maksimum. {property} harus kurang dari {value}", - "dateGreaterThanEqualToday": "{property} harus lebih besar atau sama dengan hari ini", - "dateLessThanEqualToday": "{property} harus kurang dari atau sama dengan hari ini", - "LessThan": "{property} memiliki kurang dari {value}", - "lessThanEqual": "{property} harus kurang dari atau sama dengan {value}", - "greaterThan": "{property} harus lebih besar dari {value}", - "greaterThanEqual": "{property} harus lebih besar dari atau sama dengan {value}" -} diff --git a/src/languages/id/role.json b/src/languages/id/role.json deleted file mode 100644 index f34629ebe..000000000 --- a/src/languages/id/role.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "list": "Daftar peran berhasil diambil.", - "get": "Detail peran berhasil diambil.", - "create": "Peran baru berhasil dibuat.", - "update": "Peran berhasil diperbarui.", - "delete": "Peran berhasil dihapus.", - "inactive": "Peran ditandai sebagai tidak aktif.", - "active": "Peran ditandai sebagai aktif.", - "error": { - "exist": "Peran dengan nama yang diberikan sudah ada.", - "used": "Peran saat ini sedang digunakan dan tidak dapat dihapus.", - "isActiveInvalid": "Status aktif tidak valid untuk peran tersebut.", - "notFound": "Peran yang diminta tidak ditemukan.", - "inactive": "Peran sudah tidak aktif." - } -} diff --git a/src/languages/id/setting.json b/src/languages/id/setting.json deleted file mode 100644 index 832b566ef..000000000 --- a/src/languages/id/setting.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "list": "Daftar pengaturan berhasil diambil.", - "get": "Detail pengaturan berhasil diambil.", - "update": "Pengaturan berhasil diperbarui.", - "core": "Pengaturan inti berhasil diperbarui.", - "error": { - "valueNotAllowed": "Nilai yang diberikan tidak diizinkan untuk pengaturan ini.", - "notFound": "Pengaturan yang diminta tidak ditemukan." - } -} diff --git a/src/languages/id/user.json b/src/languages/id/user.json deleted file mode 100644 index 7636093e3..000000000 --- a/src/languages/id/user.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "list": "Daftar pengguna berhasil diambil.", - "listHistory": "Riwayat pengguna berhasil diambil.", - "listPassword": "Riwayat kata sandi pengguna berhasil diambil.", - "get": "Detail pengguna berhasil diambil.", - "create": "Pengguna baru berhasil dibuat.", - "inactive": "Pengguna ditandai sebagai tidak aktif.", - "active": "Pengguna ditandai sebagai aktif.", - "blocked": "Pengguna berhasil diblokir.", - "updatePassword": "Kata sandi pengguna berhasil diperbarui.", - "signUp": "Pendaftaran pengguna berhasil.", - "loginWithCredential": "Pengguna berhasil masuk dengan kredensial.", - "loginWithSocialGoogle": "Pengguna berhasil masuk dengan login sosial Google.", - "loginWithSocialApple": "Pengguna berhasil masuk dengan login sosial Apple.", - "refresh": "Token berhasil diperbarui.", - "changePassword": "Kata sandi pengguna berhasil diubah.", - "profile": "Profil pengguna berhasil diambil.", - "updateProfile": "Profil pengguna berhasil diperbarui.", - "updateProfileUpload": "Foto profil pengguna berhasil diunggah.", - "deleteSelf": "Pengguna berhasil dihapus.", - "error": { - "mobileNumberExist": "Nomor ponsel sudah ada.", - "emailExist": "Email sudah ada.", - "blocked": "Pengguna diblokir.", - "deleted": "Pengguna telah dihapus.", - "inactive": "Pengguna tidak aktif.", - "passwordExpired": "Kata sandi telah kedaluwarsa.", - "passwordAttemptMax": "Percobaan kata sandi maksimum terlampaui.", - "passwordNotMatch": "Kata sandi tidak cocok.", - "passwordMustNew": "Kata sandi baru harus berbeda dari kata sandi sebelumnya.", - "mobileNumberNotAllowed": "Nomor ponsel tidak diizinkan.", - "blockedInvalid": "Status blokir tidak valid.", - "notFound": "Pengguna tidak ditemukan.", - "statusInvalid": "Status pengguna tidak valid." - } -} diff --git a/src/main.ts b/src/main.ts index afb31d680..95091ba50 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,15 +7,21 @@ import swaggerInit from 'src/swagger'; import { plainToInstance } from 'class-transformer'; import { AppEnvDto } from 'src/app/dtos/app.env.dto'; import { MessageService } from 'src/common/message/services/message.service'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; async function bootstrap() { - const app: NestApplication = await NestFactory.create(AppModule); + const app: NestApplication = await NestFactory.create(AppModule, { + abortOnError: false, + }); const configService = app.get(ConfigService); const databaseUri: string = configService.get('database.uri'); const env: string = configService.get('app.env'); const timezone: string = configService.get('app.timezone'); const host: string = configService.get('app.http.host'); - const port: number = configService.get('app.http.port'); + const port: number = + env !== ENUM_APP_ENVIRONMENT.MIGRATION + ? configService.get('app.http.port') + : 9999; const globalPrefix: string = configService.get('app.globalPrefix'); const versioningPrefix: string = configService.get( 'app.urlVersion.prefix' @@ -29,7 +35,7 @@ async function bootstrap() { ); const jobEnable: boolean = configService.get('app.jobEnable'); - const logger = new Logger(); + const logger = new Logger('NestJs-Main'); process.env.NODE_ENV = env; process.env.TZ = timezone; @@ -48,44 +54,58 @@ async function bootstrap() { }); } - // Swagger - await swaggerInit(app); - - // Listen - await app.listen(port, host); - - logger.log(`==========================================================`); - - logger.log(`Environment Variable`, 'NestApplication'); - // Validate Env const classEnv = plainToInstance(AppEnvDto, process.env); const errors = await validate(classEnv); if (errors.length > 0) { const messageService = app.get(MessageService); const errorsMessage = messageService.setValidationMessage(errors); - logger.log(errorsMessage, 'NestApplication'); + logger.log(errorsMessage); + throw new Error('Env Variable Invalid'); } - logger.log(JSON.parse(JSON.stringify(process.env)), 'NestApplication'); + // Swagger + await swaggerInit(app); + + // Listen + await app.listen(port, host); + + logger.log(`==========================================================`); + + logger.log(`Environment Variable`); + + logger.log(JSON.parse(JSON.stringify(process.env))); logger.log(`==========================================================`); - logger.log(`Job is ${jobEnable}`, 'NestApplication'); + if (env === ENUM_APP_ENVIRONMENT.MIGRATION) { + logger.log(`On migrate the schema`); + + await app.close(); + + logger.log(`Migrate done`); + logger.log( + `==========================================================` + ); + + return; + } + + logger.log(`Job is ${jobEnable}`); logger.log( `Http is ${httpEnable}, ${ httpEnable ? 'routes registered' : 'no routes registered' }`, 'NestApplication' ); - logger.log(`Http versioning is ${versionEnable}`, 'NestApplication'); + logger.log(`Http versioning is ${versionEnable}`); logger.log( `Http Server running on ${await app.getUrl()}`, 'NestApplication' ); - logger.log(`Database uri ${databaseUri}`, 'NestApplication'); + logger.log(`Database uri ${databaseUri}`); logger.log(`==========================================================`); } diff --git a/src/migration/data/country.json b/src/migration/data/country.json deleted file mode 100644 index 79fb78988..000000000 --- a/src/migration/data/country.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "name": "Indonesia", - "alpha2Code": "ID", - "alpha3Code": "IDN", - "domain": "id", - "fipsCode": "ID", - "numericCode": "360", - "phoneCode": ["62"], - "continent": "Asia", - "timeZone": "Asia/Jakarta" - }, - { - "name": "Singapore", - "alpha2Code": "SG", - "alpha3Code": "SGP", - "domain": "sg", - "fipsCode": "SN", - "numericCode": "702", - "phoneCode": ["65"], - "continent": "Asia", - "timeZone": "Asia/Singapore" - } -] diff --git a/src/migration/migration.module.ts b/src/migration/migration.module.ts index 186282571..47cc0befe 100644 --- a/src/migration/migration.module.ts +++ b/src/migration/migration.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { CommandModule } from 'nestjs-command'; -import { ApiKeyModule } from 'src/common/api-key/api-key.module'; -import { AuthModule } from 'src/common/auth/auth.module'; import { CommonModule } from 'src/common/common.module'; import { MigrationApiKeySeed } from 'src/migration/seeds/migration.api-key.seed'; import { MigrationCountrySeed } from 'src/migration/seeds/migration.country.seed'; import { MigrationEmailSeed } from 'src/migration/seeds/migration.email.seed'; import { MigrationRoleSeed } from 'src/migration/seeds/migration.role.seed'; import { MigrationUserSeed } from 'src/migration/seeds/migration.user.seed'; +import { ApiKeyModule } from 'src/modules/api-key/api-key.module'; +import { AuthModule } from 'src/modules/auth/auth.module'; import { CountryModule } from 'src/modules/country/country.module'; import { EmailModule } from 'src/modules/email/email.module'; import { RoleModule } from 'src/modules/role/role.module'; diff --git a/src/migration/seeds/migration.api-key.seed.ts b/src/migration/seeds/migration.api-key.seed.ts index e4a690894..273236dc2 100644 --- a/src/migration/seeds/migration.api-key.seed.ts +++ b/src/migration/seeds/migration.api-key.seed.ts @@ -1,7 +1,7 @@ import { Command } from 'nestjs-command'; import { Injectable } from '@nestjs/common'; -import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; @Injectable() export class MigrationApiKeySeed { @@ -13,20 +13,20 @@ export class MigrationApiKeySeed { }) async seeds(): Promise { try { - const apiKeyPublicKey = 'v8VB0yY887lMpTA2VJMV'; - const apiKeyPublicSecret = 'zeZbtGTugBTn3Qd5UXtSZBwt7gn3bg'; + const apiKeyDefaultKey = 'v8VB0yY887lMpTA2VJMV'; + const apiKeyDefaultSecret = 'zeZbtGTugBTn3Qd5UXtSZBwt7gn3bg'; await this.apiKeyService.createRaw({ - name: 'Api Key Public Migration', - type: ENUM_API_KEY_TYPE.PUBLIC, - key: apiKeyPublicKey, - secret: apiKeyPublicSecret, + name: 'Api Key Default Migration', + type: ENUM_API_KEY_TYPE.DEFAULT, + key: apiKeyDefaultKey, + secret: apiKeyDefaultSecret, }); const apiKeyPrivateKey = 'OgXYkQyOtP7Zl5uCbKd8'; const apiKeyPrivateSecret = '3kh0hW7pIAH3wW9DwUGrP8Y5RW9Ywv'; await this.apiKeyService.createRaw({ - name: 'Api Key Private Migration', - type: ENUM_API_KEY_TYPE.PRIVATE, + name: 'Api Key System Migration', + type: ENUM_API_KEY_TYPE.SYSTEM, key: apiKeyPrivateKey, secret: apiKeyPrivateSecret, }); diff --git a/src/migration/seeds/migration.country.seed.ts b/src/migration/seeds/migration.country.seed.ts index 0ae0c33df..73e54f38e 100644 --- a/src/migration/seeds/migration.country.seed.ts +++ b/src/migration/seeds/migration.country.seed.ts @@ -1,8 +1,6 @@ import { Command } from 'nestjs-command'; import { Injectable } from '@nestjs/common'; import { CountryService } from 'src/modules/country/services/country.service'; -import path from 'path'; -import { readFileSync } from 'fs'; import { CountryCreateRequestDto } from 'src/modules/country/dtos/request/country.create.request.dto'; @Injectable() @@ -15,13 +13,21 @@ export class MigrationCountrySeed { }) async seeds(): Promise { try { - const data = readFileSync( - path.resolve(__dirname, '../data/country.json'), - 'utf8' - ); - const countries = JSON.parse(data) as CountryCreateRequestDto[]; + const data: CountryCreateRequestDto[] = [ + { + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + }, + ]; - await this.countryService.createMany(countries); + await this.countryService.createMany(data); } catch (err: any) { throw new Error(err.message); } diff --git a/src/migration/seeds/migration.email.seed.ts b/src/migration/seeds/migration.email.seed.ts index 7a54c3e94..b35d433f8 100644 --- a/src/migration/seeds/migration.email.seed.ts +++ b/src/migration/seeds/migration.email.seed.ts @@ -13,11 +13,47 @@ export class MigrationEmailSeed { async migrate(): Promise { try { await this.emailService.createWelcome(); - } catch (err: any) {} + } catch (err: any) { + throw new Error(err); + } try { await this.emailService.createChangePassword(); - } catch (err: any) {} + } catch (err: any) { + throw new Error(err); + } + + try { + await this.emailService.createTempPassword(); + } catch (err: any) { + throw new Error(err); + } + + return; + } + + @Command({ + command: 'rollback:email', + describe: 'rollback emails', + }) + async rollback(): Promise { + try { + await this.emailService.deleteWelcome(); + } catch (err: any) { + throw new Error(err); + } + + try { + await this.emailService.deleteChangePassword(); + } catch (err: any) { + throw new Error(err); + } + + try { + await this.emailService.deleteTempPassword(); + } catch (err: any) { + throw new Error(err); + } return; } diff --git a/src/migration/seeds/migration.role.seed.ts b/src/migration/seeds/migration.role.seed.ts index b45c920be..316dee561 100644 --- a/src/migration/seeds/migration.role.seed.ts +++ b/src/migration/seeds/migration.role.seed.ts @@ -4,7 +4,7 @@ import { ENUM_POLICY_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; import { RoleService } from 'src/modules/role/services/role.service'; import { RoleCreateRequestDto } from 'src/modules/role/dtos/request/role.create.request.dto'; @@ -17,7 +17,7 @@ export class MigrationRoleSeed { describe: 'seed roles', }) async seeds(): Promise { - const dataAdmin: RoleCreateRequestDto[] = [ + const data: RoleCreateRequestDto[] = [ { name: 'superadmin', type: ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN, @@ -34,12 +34,7 @@ export class MigrationRoleSeed { { name: 'member', type: ENUM_POLICY_ROLE_TYPE.USER, - permissions: [ - { - subject: ENUM_POLICY_SUBJECT.API_KEY, - action: [ENUM_POLICY_ACTION.MANAGE], - }, - ], + permissions: [], }, { name: 'user', @@ -49,9 +44,9 @@ export class MigrationRoleSeed { ]; try { - await this.roleService.createMany(dataAdmin); + await this.roleService.createMany(data); } catch (err: any) { - throw new Error(err.message); + throw new Error(err); } return; @@ -65,7 +60,7 @@ export class MigrationRoleSeed { try { await this.roleService.deleteMany({}); } catch (err: any) { - throw new Error(err.message); + throw new Error(err); } return; diff --git a/src/migration/seeds/migration.user.seed.ts b/src/migration/seeds/migration.user.seed.ts index 8cfa14ce8..03a8e0719 100644 --- a/src/migration/seeds/migration.user.seed.ts +++ b/src/migration/seeds/migration.user.seed.ts @@ -1,24 +1,19 @@ import { Command } from 'nestjs-command'; import { Injectable } from '@nestjs/common'; -import { AuthService } from 'src/common/auth/services/auth.service'; +import { AuthService } from 'src/modules/auth/services/auth.service'; import { UserService } from 'src/modules/user/services/user.service'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; import { RoleService } from 'src/modules/role/services/role.service'; -import { ENUM_USER_SIGN_UP_FROM } from 'src/modules/user/constants/user.enum.constant'; +import { ENUM_USER_SIGN_UP_FROM } from 'src/modules/user/enums/user.enum'; import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; import { CountryService } from 'src/modules/country/services/country.service'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; -import { UserPasswordHistoryService } from 'src/modules/user/services/user-password-history.service'; -import { UserStateHistoryService } from 'src/modules/user/services/user-state-history.service'; +import { faker } from '@faker-js/faker'; @Injectable() export class MigrationUserSeed { constructor( private readonly authService: AuthService, private readonly userService: UserService, - private readonly userPasswordHistoryService: UserPasswordHistoryService, - private readonly userStateHistoryService: UserStateHistoryService, private readonly roleService: RoleService, private readonly countryService: CountryService ) {} @@ -34,15 +29,14 @@ export class MigrationUserSeed { await this.roleService.findOneByName('superadmin'); const adminRole: RoleDoc = await this.roleService.findOneByName('admin'); + const country: CountryDoc = + await this.countryService.findOneByAlpha2('ID'); + const memberRole: RoleDoc = await this.roleService.findOneByName('member'); const userRole: RoleDoc = await this.roleService.findOneByName('user'); - const country: CountryDoc = await this.countryService.findOneByAlpha2( - ENUM_MESSAGE_LANGUAGE.EN - ); - try { - const user1: UserDoc = await this.userService.create( + await this.userService.create( { role: superAdminRole._id, name: 'superadmin', @@ -50,10 +44,10 @@ export class MigrationUserSeed { country: country._id, }, passwordHash, - ENUM_USER_SIGN_UP_FROM.ADMIN + ENUM_USER_SIGN_UP_FROM.SEED ); - const user2: UserDoc = await this.userService.create( + await this.userService.create( { role: adminRole._id, name: 'admin', @@ -61,51 +55,49 @@ export class MigrationUserSeed { country: country._id, }, passwordHash, - ENUM_USER_SIGN_UP_FROM.ADMIN + ENUM_USER_SIGN_UP_FROM.SEED ); - const user3: UserDoc = await this.userService.create( + + await this.userService.create( { - role: userRole._id, - name: 'user', - email: 'user@mail.com', + role: memberRole._id, + name: 'member', + email: 'member@mail.com', country: country._id, }, passwordHash, - ENUM_USER_SIGN_UP_FROM.ADMIN + ENUM_USER_SIGN_UP_FROM.SEED ); - const user4: UserDoc = await this.userService.create( + await this.userService.create( { - role: memberRole._id, - name: 'member', - email: 'member@mail.com', + role: userRole._id, + name: 'user', + email: 'user@mail.com', country: country._id, }, passwordHash, - ENUM_USER_SIGN_UP_FROM.ADMIN + ENUM_USER_SIGN_UP_FROM.SEED ); - await this.userStateHistoryService.createCreated(user1, user1._id); - await this.userStateHistoryService.createCreated(user2, user2._id); - await this.userStateHistoryService.createCreated(user3, user3._id); - await this.userStateHistoryService.createCreated(user4, user4._id); - await this.userPasswordHistoryService.createByAdmin( - user1, - user1._id - ); - await this.userPasswordHistoryService.createByAdmin( - user2, - user1._id - ); - await this.userPasswordHistoryService.createByAdmin( - user3, - user1._id - ); - await this.userPasswordHistoryService.createByAdmin( - user4, - user1._id - ); + // Add random user + const randomUser = Array(30) + .fill(0) + .map(() => + this.userService.create( + { + role: userRole._id, + name: faker.person.fullName(), + email: faker.internet.email(), + country: country._id, + }, + passwordHash, + ENUM_USER_SIGN_UP_FROM.SEED + ) + ); + + await Promise.all(randomUser); } catch (err: any) { - throw new Error(err.message); + throw new Error(err); } return; @@ -119,7 +111,7 @@ export class MigrationUserSeed { try { await this.userService.deleteMany({}); } catch (err: any) { - throw new Error(err.message); + throw new Error(err); } return; diff --git a/src/common/api-key/api-key.module.ts b/src/modules/api-key/api-key.module.ts similarity index 62% rename from src/common/api-key/api-key.module.ts rename to src/modules/api-key/api-key.module.ts index c06014faa..48c194dfa 100644 --- a/src/common/api-key/api-key.module.ts +++ b/src/modules/api-key/api-key.module.ts @@ -1,7 +1,7 @@ import { DynamicModule, Module } from '@nestjs/common'; -import { ApiKeyXApiKeyStrategy } from 'src/common/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy'; -import { ApiKeyRepositoryModule } from 'src/common/api-key/repository/api-key.repository.module'; -import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ApiKeyXApiKeyStrategy } from 'src/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy'; +import { ApiKeyRepositoryModule } from 'src/modules/api-key/repository/api-key.repository.module'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; @Module({ providers: [ApiKeyService], diff --git a/src/common/api-key/constants/api-key.constant.ts b/src/modules/api-key/constants/api-key.constant.ts similarity index 100% rename from src/common/api-key/constants/api-key.constant.ts rename to src/modules/api-key/constants/api-key.constant.ts diff --git a/src/common/api-key/constants/api-key.doc.constant.ts b/src/modules/api-key/constants/api-key.doc.constant.ts similarity index 89% rename from src/common/api-key/constants/api-key.doc.constant.ts rename to src/modules/api-key/constants/api-key.doc.constant.ts index 73fb0d793..f967694af 100644 --- a/src/common/api-key/constants/api-key.doc.constant.ts +++ b/src/modules/api-key/constants/api-key.doc.constant.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; export const ApiKeyDocQueryIsActive = [ { diff --git a/src/common/api-key/constants/api-key.list.constant.ts b/src/modules/api-key/constants/api-key.list.constant.ts similarity index 68% rename from src/common/api-key/constants/api-key.list.constant.ts rename to src/modules/api-key/constants/api-key.list.constant.ts index 5885d4569..f74a9842b 100644 --- a/src/common/api-key/constants/api-key.list.constant.ts +++ b/src/modules/api-key/constants/api-key.list.constant.ts @@ -1,4 +1,4 @@ -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; export const API_KEY_DEFAULT_AVAILABLE_SEARCH = ['name', 'key']; export const API_KEY_DEFAULT_IS_ACTIVE = [true, false]; diff --git a/src/common/api-key/controllers/api-key.admin.controller.ts b/src/modules/api-key/controllers/api-key.admin.controller.ts similarity index 78% rename from src/common/api-key/controllers/api-key.admin.controller.ts rename to src/modules/api-key/controllers/api-key.admin.controller.ts index c5c8efde3..a8cbfe7a2 100644 --- a/src/common/api-key/controllers/api-key.admin.controller.ts +++ b/src/modules/api-key/controllers/api-key.admin.controller.ts @@ -25,13 +25,13 @@ import { IResponse, IResponsePaging, } from 'src/common/response/interfaces/response.interface'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; import { API_KEY_DEFAULT_AVAILABLE_SEARCH, API_KEY_DEFAULT_IS_ACTIVE, API_KEY_DEFAULT_TYPE, -} from 'src/common/api-key/constants/api-key.list.constant'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; +} from 'src/modules/api-key/constants/api-key.list.constant'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { ApiKeyAdminActiveDoc, ApiKeyAdminCreateDoc, @@ -42,32 +42,29 @@ import { ApiKeyAdminResetDoc, ApiKeyAdminUpdateDateDoc, ApiKeyAdminUpdateDoc, -} from 'src/common/api-key/docs/api-key.admin.doc'; -import { ApiKeyCreateRequestDto } from 'src/common/api-key/dtos/request/api-key.create.request.dto'; -import { ApiKeyUpdateDateRequestDto } from 'src/common/api-key/dtos/request/api-key.update-date.request.dto'; -import { ApiKeyUpdateRequestDto } from 'src/common/api-key/dtos/request/api-key.update.request.dto'; -import { ApiKeyCreateResponseDto } from 'src/common/api-key/dtos/response/api-key.create.dto'; -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; -import { ApiKeyListResponseDto } from 'src/common/api-key/dtos/response/api-key.list.response.dto'; -import { ApiKeyResetResponseDto } from 'src/common/api-key/dtos/response/api-key.reset.dto'; -import { - ApiKeyActivePipe, - ApiKeyInactivePipe, -} from 'src/common/api-key/pipes/api-key.is-active.pipe'; -import { ApiKeyParsePipe } from 'src/common/api-key/pipes/api-key.parse.pipe'; -import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; -import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; -import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +} from 'src/modules/api-key/docs/api-key.admin.doc'; +import { ApiKeyCreateRequestDto } from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { ApiKeyResetResponseDto } from 'src/modules/api-key/dtos/response/api-key.reset.dto'; +import { ApiKeyParsePipe } from 'src/modules/api-key/pipes/api-key.parse.pipe'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; import { ENUM_POLICY_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; import { PolicyAbilityProtected, PolicyRoleProtected, -} from 'src/common/policy/decorators/policy.decorator'; -import { ApiKeyNotExpiredPipe } from 'src/common/api-key/pipes/api-key.expired.pipe'; +} from 'src/modules/policy/decorators/policy.decorator'; +import { ApiKeyNotExpiredPipe } from 'src/modules/api-key/pipes/api-key.expired.pipe'; +import { ApiKeyIsActivePipe } from 'src/modules/api-key/pipes/api-key.is-active.pipe'; @ApiTags('common.admin.apiKey') @Controller({ @@ -88,7 +85,7 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/list') async list( @PaginationQuery({ @@ -138,7 +135,7 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/get/:apiKey') async get( @Param('apiKey', RequestRequiredPipe, ApiKeyParsePipe) @@ -156,7 +153,7 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Post('/create') async create( @Body() body: ApiKeyCreateRequestDto @@ -177,14 +174,14 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:apiKey/reset') async reset( @Param( 'apiKey', RequestRequiredPipe, ApiKeyParsePipe, - ApiKeyActivePipe, + new ApiKeyIsActivePipe([true]), ApiKeyNotExpiredPipe ) apiKey: ApiKeyDoc @@ -205,7 +202,7 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Put('/update/:apiKey') async update( @Body() body: ApiKeyUpdateRequestDto, @@ -213,7 +210,7 @@ export class ApiKeyAdminController { 'apiKey', RequestRequiredPipe, ApiKeyParsePipe, - ApiKeyActivePipe, + new ApiKeyIsActivePipe([true]), ApiKeyNotExpiredPipe ) apiKey: ApiKeyDoc @@ -231,14 +228,14 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:apiKey/inactive') async inactive( @Param( 'apiKey', RequestRequiredPipe, ApiKeyParsePipe, - ApiKeyActivePipe, + new ApiKeyIsActivePipe([true]), ApiKeyNotExpiredPipe ) apiKey: ApiKeyDoc @@ -256,14 +253,14 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:apiKey/active') async active( @Param( 'apiKey', RequestRequiredPipe, ApiKeyParsePipe, - ApiKeyInactivePipe, + new ApiKeyIsActivePipe([false]), ApiKeyNotExpiredPipe ) apiKey: ApiKeyDoc @@ -281,11 +278,16 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Put('/update/:apiKey/date') async updateDate( @Body() body: ApiKeyUpdateDateRequestDto, - @Param('apiKey', RequestRequiredPipe, ApiKeyParsePipe, ApiKeyActivePipe) + @Param( + 'apiKey', + RequestRequiredPipe, + ApiKeyParsePipe, + new ApiKeyIsActivePipe([true]) + ) apiKey: ApiKeyDoc ): Promise> { await this.apiKeyService.updateDate(apiKey, body); @@ -301,7 +303,7 @@ export class ApiKeyAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Delete('/delete/:apiKey') async delete( @Param('apiKey', RequestRequiredPipe, ApiKeyParsePipe) diff --git a/src/common/api-key/decorators/api-key.decorator.ts b/src/modules/api-key/decorators/api-key.decorator.ts similarity index 59% rename from src/common/api-key/decorators/api-key.decorator.ts rename to src/modules/api-key/decorators/api-key.decorator.ts index 698d41bbf..7ec65409f 100644 --- a/src/common/api-key/decorators/api-key.decorator.ts +++ b/src/modules/api-key/decorators/api-key.decorator.ts @@ -6,11 +6,11 @@ import { UseGuards, } from '@nestjs/common'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { API_KEY_X_TYPE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; -import { ApiKeyXApiKeyGuard } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.guard'; -import { ApiKeyXApiKeyTypeGuard } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.type.guard'; -import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface'; +import { API_KEY_X_TYPE_META_KEY } from 'src/modules/api-key/constants/api-key.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; +import { ApiKeyXApiKeyGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard'; +import { ApiKeyXApiKeyTypeGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard'; +import { IApiKeyPayload } from 'src/modules/api-key/interfaces/api-key.interface'; export const ApiKeyPayload: () => ParameterDecorator = createParamDecorator( (data: string, ctx: ExecutionContext): T => { @@ -21,16 +21,16 @@ export const ApiKeyPayload: () => ParameterDecorator = createParamDecorator( } ); -export function ApiKeyPrivateProtected(): MethodDecorator { +export function ApiKeySystemProtected(): MethodDecorator { return applyDecorators( UseGuards(ApiKeyXApiKeyGuard, ApiKeyXApiKeyTypeGuard), - SetMetadata(API_KEY_X_TYPE_META_KEY, [ENUM_API_KEY_TYPE.PRIVATE]) + SetMetadata(API_KEY_X_TYPE_META_KEY, [ENUM_API_KEY_TYPE.SYSTEM]) ); } -export function ApiKeyPublicProtected(): MethodDecorator { +export function ApiKeyProtected(): MethodDecorator { return applyDecorators( UseGuards(ApiKeyXApiKeyGuard, ApiKeyXApiKeyTypeGuard), - SetMetadata(API_KEY_X_TYPE_META_KEY, [ENUM_API_KEY_TYPE.PUBLIC]) + SetMetadata(API_KEY_X_TYPE_META_KEY, [ENUM_API_KEY_TYPE.DEFAULT]) ); } diff --git a/src/common/api-key/docs/api-key.admin.doc.ts b/src/modules/api-key/docs/api-key.admin.doc.ts similarity index 87% rename from src/common/api-key/docs/api-key.admin.doc.ts rename to src/modules/api-key/docs/api-key.admin.doc.ts index 78642b371..803a42d6b 100644 --- a/src/common/api-key/docs/api-key.admin.doc.ts +++ b/src/modules/api-key/docs/api-key.admin.doc.ts @@ -1,6 +1,4 @@ import { applyDecorators, HttpStatus } from '@nestjs/common'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { Doc, DocAuth, @@ -12,19 +10,21 @@ import { DocResponse, DocResponsePaging, } from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; import { ApiKeyDocParamsId, ApiKeyDocQueryIsActive, ApiKeyDocQueryType, -} from 'src/common/api-key/constants/api-key.doc.constant'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ApiKeyCreateRequestDto } from 'src/common/api-key/dtos/request/api-key.create.request.dto'; -import { ApiKeyUpdateDateRequestDto } from 'src/common/api-key/dtos/request/api-key.update-date.request.dto'; -import { ApiKeyUpdateRequestDto } from 'src/common/api-key/dtos/request/api-key.update.request.dto'; -import { ApiKeyCreateResponseDto } from 'src/common/api-key/dtos/response/api-key.create.dto'; -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; -import { ApiKeyListResponseDto } from 'src/common/api-key/dtos/response/api-key.list.response.dto'; -import { ApiKeyResetResponseDto } from 'src/common/api-key/dtos/response/api-key.reset.dto'; +} from 'src/modules/api-key/constants/api-key.doc.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyCreateRequestDto } from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { ApiKeyResetResponseDto } from 'src/modules/api-key/dtos/response/api-key.reset.dto'; +import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; export function ApiKeyAdminListDoc(): MethodDecorator { return applyDecorators( @@ -60,7 +60,7 @@ export function ApiKeyAdminGetDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), ]) @@ -101,17 +101,17 @@ export function ApiKeyAdminActiveDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), DocOneOf( HttpStatus.BAD_REQUEST, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, messagePath: 'apiKey.error.expired', }, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, messagePath: 'apiKey.error.isActiveInvalid', } ), @@ -134,17 +134,17 @@ export function ApiKeyAdminInactiveDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), DocOneOf( HttpStatus.BAD_REQUEST, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, messagePath: 'apiKey.error.expired', }, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, messagePath: 'apiKey.error.isActiveInvalid', } ), @@ -169,17 +169,17 @@ export function ApiKeyAdminResetDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), DocOneOf( HttpStatus.BAD_REQUEST, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, messagePath: 'apiKey.error.expired', }, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, messagePath: 'apiKey.error.isActiveInvalid', } ), @@ -204,17 +204,17 @@ export function ApiKeyAdminUpdateDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), DocOneOf( HttpStatus.BAD_REQUEST, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, messagePath: 'apiKey.error.expired', }, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, messagePath: 'apiKey.error.isActiveInvalid', } ), @@ -241,17 +241,17 @@ export function ApiKeyAdminUpdateDateDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'apiKey.error.notFound', }), DocOneOf( HttpStatus.BAD_REQUEST, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, messagePath: 'apiKey.error.expired', }, { - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, messagePath: 'apiKey.error.isActiveInvalid', } ), diff --git a/src/modules/api-key/dtos/api-key.payload.dto.ts b/src/modules/api-key/dtos/api-key.payload.dto.ts new file mode 100644 index 000000000..4afcb6ecb --- /dev/null +++ b/src/modules/api-key/dtos/api-key.payload.dto.ts @@ -0,0 +1,9 @@ +import { PickType } from '@nestjs/swagger'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; + +export class ApiKeyPayloadDto extends PickType(ApiKeyGetResponseDto, [ + '_id', + 'name', + 'type', + 'key', +] as const) {} diff --git a/src/common/api-key/dtos/request/api-key.create.request.dto.ts b/src/modules/api-key/dtos/request/api-key.create.request.dto.ts similarity index 75% rename from src/common/api-key/dtos/request/api-key.create.request.dto.ts rename to src/modules/api-key/dtos/request/api-key.create.request.dto.ts index cae02217e..47435d371 100644 --- a/src/common/api-key/dtos/request/api-key.create.request.dto.ts +++ b/src/modules/api-key/dtos/request/api-key.create.request.dto.ts @@ -1,9 +1,9 @@ import { faker } from '@faker-js/faker'; import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, MaxLength } from 'class-validator'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; -import { ApiKeyUpdateDateRequestDto } from 'src/common/api-key/dtos/request/api-key.update-date.request.dto'; -import { ApiKeyUpdateRequestDto } from 'src/common/api-key/dtos/request/api-key.update.request.dto'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; export class ApiKeyCreateRequestDto extends IntersectionType( ApiKeyUpdateRequestDto, @@ -11,7 +11,7 @@ export class ApiKeyCreateRequestDto extends IntersectionType( ) { @ApiProperty({ description: 'Api Key name', - example: ENUM_API_KEY_TYPE.PUBLIC, + example: ENUM_API_KEY_TYPE.DEFAULT, required: true, enum: ENUM_API_KEY_TYPE, }) diff --git a/src/common/api-key/dtos/request/api-key.update-date.request.dto.ts b/src/modules/api-key/dtos/request/api-key.update-date.request.dto.ts similarity index 100% rename from src/common/api-key/dtos/request/api-key.update-date.request.dto.ts rename to src/modules/api-key/dtos/request/api-key.update-date.request.dto.ts diff --git a/src/common/api-key/dtos/request/api-key.update.request.dto.ts b/src/modules/api-key/dtos/request/api-key.update.request.dto.ts similarity index 100% rename from src/common/api-key/dtos/request/api-key.update.request.dto.ts rename to src/modules/api-key/dtos/request/api-key.update.request.dto.ts diff --git a/src/common/api-key/dtos/response/api-key.create.dto.ts b/src/modules/api-key/dtos/response/api-key.create.dto.ts similarity index 76% rename from src/common/api-key/dtos/response/api-key.create.dto.ts rename to src/modules/api-key/dtos/response/api-key.create.dto.ts index 26d111b88..1df9e1675 100644 --- a/src/common/api-key/dtos/response/api-key.create.dto.ts +++ b/src/modules/api-key/dtos/response/api-key.create.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, PickType } from '@nestjs/swagger'; -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; export class ApiKeyCreateResponseDto extends PickType(ApiKeyGetResponseDto, [ 'key', diff --git a/src/common/api-key/dtos/response/api-key.get.response.dto.ts b/src/modules/api-key/dtos/response/api-key.get.response.dto.ts similarity index 59% rename from src/common/api-key/dtos/response/api-key.get.response.dto.ts rename to src/modules/api-key/dtos/response/api-key.get.response.dto.ts index 95c780ce5..d290626b7 100644 --- a/src/common/api-key/dtos/response/api-key.get.response.dto.ts +++ b/src/modules/api-key/dtos/response/api-key.get.response.dto.ts @@ -1,9 +1,10 @@ import { faker } from '@faker-js/faker'; import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { Exclude } from 'class-transformer'; -import { ApiKeyPayloadDto } from 'src/common/api-key/dtos/api-key.payload.dto'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; -export class ApiKeyGetResponseDto extends ApiKeyPayloadDto { +export class ApiKeyGetResponseDto extends DatabaseDto { @ApiHideProperty() @Exclude() hash: string; @@ -33,22 +34,26 @@ export class ApiKeyGetResponseDto extends ApiKeyPayloadDto { endDate?: Date; @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), + description: 'Alias name of api key', + example: faker.person.jobTitle(), required: true, nullable: false, }) - readonly createdAt: Date; + name: string; @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), + description: 'Type of api key', + example: ENUM_API_KEY_TYPE.DEFAULT, required: true, nullable: false, }) - readonly updatedAt: Date; + type: ENUM_API_KEY_TYPE; - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; + @ApiProperty({ + description: 'Unique key of api key', + example: faker.string.alpha(15), + required: true, + nullable: false, + }) + key: string; } diff --git a/src/modules/api-key/dtos/response/api-key.list.response.dto.ts b/src/modules/api-key/dtos/response/api-key.list.response.dto.ts new file mode 100644 index 000000000..2d9f91e33 --- /dev/null +++ b/src/modules/api-key/dtos/response/api-key.list.response.dto.ts @@ -0,0 +1,3 @@ +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; + +export class ApiKeyListResponseDto extends ApiKeyGetResponseDto {} diff --git a/src/modules/api-key/dtos/response/api-key.reset.dto.ts b/src/modules/api-key/dtos/response/api-key.reset.dto.ts new file mode 100644 index 000000000..3d102c02b --- /dev/null +++ b/src/modules/api-key/dtos/response/api-key.reset.dto.ts @@ -0,0 +1,3 @@ +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; + +export class ApiKeyResetResponseDto extends ApiKeyCreateResponseDto {} diff --git a/src/modules/api-key/enums/api-key.enum.ts b/src/modules/api-key/enums/api-key.enum.ts new file mode 100644 index 000000000..f0bd8659c --- /dev/null +++ b/src/modules/api-key/enums/api-key.enum.ts @@ -0,0 +1,4 @@ +export enum ENUM_API_KEY_TYPE { + SYSTEM = 'SYSTEM', + DEFAULT = 'DEFAULT', +} diff --git a/src/modules/api-key/enums/api-key.status-code.enum.ts b/src/modules/api-key/enums/api-key.status-code.enum.ts new file mode 100644 index 000000000..d6474c549 --- /dev/null +++ b/src/modules/api-key/enums/api-key.status-code.enum.ts @@ -0,0 +1,11 @@ +export enum ENUM_API_KEY_STATUS_CODE_ERROR { + X_API_KEY_REQUIRED = 5050, + X_API_KEY_NOT_FOUND = 5051, + X_API_KEY_INACTIVE = 5052, + X_API_KEY_EXPIRED = 5053, + X_API_KEY_INVALID = 5054, + X_API_KEY_FORBIDDEN = 5055, + IS_ACTIVE = 5056, + EXPIRED = 5057, + NOT_FOUND = 5058, +} diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts b/src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.ts similarity index 70% rename from src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts rename to src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.ts index e39fada8d..c187effd9 100644 --- a/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts +++ b/src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.ts @@ -1,19 +1,14 @@ import { AuthGuard } from '@nestjs/passport'; import { - ExecutionContext, ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { BadRequestError } from 'passport-headerapikey'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; @Injectable() export class ApiKeyXApiKeyGuard extends AuthGuard('x-api-key') { - canActivate(context: ExecutionContext) { - return super.canActivate(context); - } - handleRequest( err: Error, apiKey: IApiKeyPayload, @@ -21,8 +16,7 @@ export class ApiKeyXApiKeyGuard extends AuthGuard('x-api-key') { ): IApiKeyPayload { if (!apiKey || info?.message === 'Missing Api Key') { throw new UnauthorizedException({ - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED, message: 'apiKey.error.xApiKey.required', }); } else if (err) { @@ -30,23 +24,21 @@ export class ApiKeyXApiKeyGuard extends AuthGuard('x-api-key') { if ( statusCode === - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND_ERROR + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND ) { throw new ForbiddenException({ statusCode, message: 'apiKey.error.xApiKey.notFound', }); } else if ( - statusCode === - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE_ERROR + statusCode === ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE ) { throw new ForbiddenException({ statusCode, message: 'apiKey.error.xApiKey.inactive', }); } else if ( - statusCode === - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED_ERROR + statusCode === ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED ) { throw new ForbiddenException({ statusCode, @@ -55,8 +47,7 @@ export class ApiKeyXApiKeyGuard extends AuthGuard('x-api-key') { } throw new UnauthorizedException({ - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID, message: 'apiKey.error.xApiKey.invalid', }); } diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts b/src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts similarity index 71% rename from src/common/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts rename to src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts index e463c7690..d4dc2f713 100644 --- a/src/common/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts +++ b/src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.ts @@ -6,9 +6,9 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { API_KEY_X_TYPE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { API_KEY_X_TYPE_META_KEY } from 'src/modules/api-key/constants/api-key.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; @Injectable() export class ApiKeyXApiKeyTypeGuard implements CanActivate { @@ -27,8 +27,7 @@ export class ApiKeyXApiKeyTypeGuard implements CanActivate { if (!required.includes(apiKey.type)) { throw new BadRequestException({ - statusCode: - ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN, message: 'apiKey.error.xApiKey.forbidden', }); } diff --git a/src/common/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts b/src/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts similarity index 86% rename from src/common/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts rename to src/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts index 73e130e9a..8824eb596 100644 --- a/src/common/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts +++ b/src/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.ts @@ -3,9 +3,9 @@ import { PassportStrategy } from '@nestjs/passport'; import Strategy from 'passport-headerapikey'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; -import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyEntity } from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; @Injectable() export class ApiKeyXApiKeyStrategy extends PassportStrategy( @@ -19,7 +19,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( super( { header: 'X-API-KEY', prefix: '' }, true, - async ( + ( xApiKey: string, verified: ( error: Error, @@ -35,7 +35,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( xApiKey: string, verified: ( error: Error, - user?: ApiKeyEntity, + user?: Record, info?: string | number ) => Promise, req: IRequestApp @@ -44,7 +44,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( if (xApiKeyArr.length !== 2) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID}` ), null, null @@ -62,7 +62,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( if (!apiKey) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND}` ), null, null @@ -72,7 +72,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( } else if (!apiKey.isActive) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE}` ), null, null @@ -80,18 +80,18 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( return; } else if (apiKey.startDate && apiKey.endDate) { - if (today < apiKey.startDate) { + if (today > apiKey.endDate) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED}` ), null, null ); - } else if (today > apiKey.endDate) { + } else if (apiKey.startDate < today) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE}` ), null, null @@ -105,7 +105,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( if (!validateApiKey) { verified( new Error( - `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID_ERROR}` + `${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID}` ), null, null @@ -116,6 +116,7 @@ export class ApiKeyXApiKeyStrategy extends PassportStrategy( req.apiKey = { _id: apiKey._id, + name: apiKey._id, key: apiKey.key, type: apiKey.type, }; diff --git a/src/common/api-key/interfaces/api-key.interface.ts b/src/modules/api-key/interfaces/api-key.interface.ts similarity index 53% rename from src/common/api-key/interfaces/api-key.interface.ts rename to src/modules/api-key/interfaces/api-key.interface.ts index 942f00adc..2564fcb5a 100644 --- a/src/common/api-key/interfaces/api-key.interface.ts +++ b/src/modules/api-key/interfaces/api-key.interface.ts @@ -1,4 +1,4 @@ -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; export interface IApiKeyPayload { _id: string; diff --git a/src/common/api-key/interfaces/api-key.service.interface.ts b/src/modules/api-key/interfaces/api-key.service.interface.ts similarity index 60% rename from src/common/api-key/interfaces/api-key.service.interface.ts rename to src/modules/api-key/interfaces/api-key.service.interface.ts index 614fe4e43..ddd2824f1 100644 --- a/src/common/api-key/interfaces/api-key.service.interface.ts +++ b/src/modules/api-key/interfaces/api-key.service.interface.ts @@ -1,43 +1,41 @@ import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, + IDatabaseUpdateManyOptions, } from 'src/common/database/interfaces/database.interface'; import { ApiKeyCreateRawRequestDto, ApiKeyCreateRequestDto, -} from 'src/common/api-key/dtos/request/api-key.create.request.dto'; -import { ApiKeyUpdateDateRequestDto } from 'src/common/api-key/dtos/request/api-key.update-date.request.dto'; -import { ApiKeyUpdateRequestDto } from 'src/common/api-key/dtos/request/api-key.update.request.dto'; -import { ApiKeyCreateResponseDto } from 'src/common/api-key/dtos/response/api-key.create.dto'; -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; -import { ApiKeyListResponseDto } from 'src/common/api-key/dtos/response/api-key.list.response.dto'; -import { ApiKeyResetResponseDto } from 'src/common/api-key/dtos/response/api-key.reset.dto'; -import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +} from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { ApiKeyResetResponseDto } from 'src/modules/api-key/dtos/response/api-key.reset.dto'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/modules/api-key/repository/entities/api-key.entity'; export interface IApiKeyService { findAll( find?: Record, options?: IDatabaseFindAllOptions ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; + findOneById(_id: string, options?: IDatabaseOptions): Promise; findOne( find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByKey( - key: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; + findOneByKey(key: string, options?: IDatabaseOptions): Promise; findOneByActiveKey( key: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; getTotal( find?: Record, @@ -90,9 +88,13 @@ export interface IApiKeyService { createHashApiKey(key: string, secret: string): Promise; deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions + ): Promise; + inactiveManyByEndDate( + options?: IDatabaseUpdateManyOptions ): Promise; - inactiveManyByEndDate(options?: IDatabaseManyOptions): Promise; - mapList(apiKeys: ApiKeyDoc[]): Promise; - mapGet(apiKey: ApiKeyDoc): Promise; + mapList( + apiKeys: ApiKeyDoc[] | ApiKeyEntity[] + ): Promise; + mapGet(apiKey: ApiKeyDoc | ApiKeyEntity): Promise; } diff --git a/src/common/api-key/pipes/api-key.expired.pipe.ts b/src/modules/api-key/pipes/api-key.expired.pipe.ts similarity index 78% rename from src/common/api-key/pipes/api-key.expired.pipe.ts rename to src/modules/api-key/pipes/api-key.expired.pipe.ts index 1a54bdacb..b0b77289b 100644 --- a/src/common/api-key/pipes/api-key.expired.pipe.ts +++ b/src/modules/api-key/pipes/api-key.expired.pipe.ts @@ -1,7 +1,7 @@ import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; @Injectable() export class ApiKeyNotExpiredPipe implements PipeTransform { @@ -12,7 +12,7 @@ export class ApiKeyNotExpiredPipe implements PipeTransform { if (value.startDate && value.endDate && today > value.endDate) { throw new BadRequestException({ - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, message: 'apiKey.error.expired', }); } diff --git a/src/modules/api-key/pipes/api-key.is-active.pipe.ts b/src/modules/api-key/pipes/api-key.is-active.pipe.ts new file mode 100644 index 000000000..e6ef066cd --- /dev/null +++ b/src/modules/api-key/pipes/api-key.is-active.pipe.ts @@ -0,0 +1,23 @@ +import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; + +@Injectable() +export class ApiKeyIsActivePipe implements PipeTransform { + private readonly isActive: boolean[]; + + constructor(isActive: boolean[]) { + this.isActive = isActive; + } + + async transform(value: ApiKeyDoc): Promise { + if (!this.isActive.includes(value.isActive)) { + throw new BadRequestException({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, + message: 'apiKey.error.isActiveInvalid', + }); + } + + return value; + } +} diff --git a/src/common/api-key/pipes/api-key.parse.pipe.ts b/src/modules/api-key/pipes/api-key.parse.pipe.ts similarity index 67% rename from src/common/api-key/pipes/api-key.parse.pipe.ts rename to src/modules/api-key/pipes/api-key.parse.pipe.ts index 4e2c81146..e9f1f10f5 100644 --- a/src/common/api-key/pipes/api-key.parse.pipe.ts +++ b/src/modules/api-key/pipes/api-key.parse.pipe.ts @@ -1,7 +1,7 @@ import { PipeTransform, Injectable, NotFoundException } from '@nestjs/common'; -import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; -import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; -import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; @Injectable() export class ApiKeyParsePipe implements PipeTransform { @@ -11,7 +11,7 @@ export class ApiKeyParsePipe implements PipeTransform { const apiKey: ApiKeyDoc = await this.apiKeyService.findOneById(value); if (!apiKey) { throw new NotFoundException({ - statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, message: 'apiKey.error.notFound', }); } diff --git a/src/common/api-key/repository/api-key.repository.module.ts b/src/modules/api-key/repository/api-key.repository.module.ts similarity index 79% rename from src/common/api-key/repository/api-key.repository.module.ts rename to src/modules/api-key/repository/api-key.repository.module.ts index 36ed13e53..317fc0752 100644 --- a/src/common/api-key/repository/api-key.repository.module.ts +++ b/src/modules/api-key/repository/api-key.repository.module.ts @@ -4,8 +4,8 @@ import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database import { ApiKeyEntity, ApiKeySchema, -} from 'src/common/api-key/repository/entities/api-key.entity'; -import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository'; +} from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyRepository } from 'src/modules/api-key/repository/repositories/api-key.repository'; @Module({ providers: [ApiKeyRepository], diff --git a/src/common/api-key/repository/entities/api-key.entity.ts b/src/modules/api-key/repository/entities/api-key.entity.ts similarity index 81% rename from src/common/api-key/repository/entities/api-key.entity.ts rename to src/modules/api-key/repository/entities/api-key.entity.ts index fe9d920fd..b71c278bd 100644 --- a/src/common/api-key/repository/entities/api-key.entity.ts +++ b/src/modules/api-key/repository/entities/api-key.entity.ts @@ -1,16 +1,16 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; -import { ENUM_API_KEY_TYPE } from 'src/common/api-key/constants/api-key.enum.constant'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; export const ApiKeyTableName = 'ApiKeys'; @DatabaseEntity({ collection: ApiKeyTableName }) -export class ApiKeyEntity extends DatabaseMongoUUIDEntityAbstract { +export class ApiKeyEntity extends DatabaseEntityAbstract { @DatabaseProp({ required: true, enum: ENUM_API_KEY_TYPE, @@ -25,7 +25,6 @@ export class ApiKeyEntity extends DatabaseMongoUUIDEntityAbstract { type: String, minlength: 1, maxlength: 100, - lowercase: true, trim: true, }) name: string; diff --git a/src/common/api-key/repository/repositories/api-key.repository.ts b/src/modules/api-key/repository/repositories/api-key.repository.ts similarity index 59% rename from src/common/api-key/repository/repositories/api-key.repository.ts rename to src/modules/api-key/repository/repositories/api-key.repository.ts index 9304b50e5..b25d74322 100644 --- a/src/common/api-key/repository/repositories/api-key.repository.ts +++ b/src/modules/api-key/repository/repositories/api-key.repository.ts @@ -1,14 +1,14 @@ import { Injectable } from '@nestjs/common'; import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/database.repository.abstract'; import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; import { ApiKeyDoc, ApiKeyEntity, -} from 'src/common/api-key/repository/entities/api-key.entity'; +} from 'src/modules/api-key/repository/entities/api-key.entity'; @Injectable() -export class ApiKeyRepository extends DatabaseMongoUUIDRepositoryAbstract< +export class ApiKeyRepository extends DatabaseRepositoryAbstract< ApiKeyEntity, ApiKeyDoc > { diff --git a/src/common/api-key/services/api-key.service.ts b/src/modules/api-key/services/api-key.service.ts similarity index 69% rename from src/common/api-key/services/api-key.service.ts rename to src/modules/api-key/services/api-key.service.ts index a81fa8e8d..338e71f08 100644 --- a/src/common/api-key/services/api-key.service.ts +++ b/src/modules/api-key/services/api-key.service.ts @@ -3,11 +3,12 @@ import { ConfigService } from '@nestjs/config'; import { plainToInstance } from 'class-transformer'; import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, + IDatabaseUpdateManyOptions, } from 'src/common/database/interfaces/database.interface'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; @@ -15,19 +16,20 @@ import { HelperStringService } from 'src/common/helper/services/helper.string.se import { ApiKeyCreateRawRequestDto, ApiKeyCreateRequestDto, -} from 'src/common/api-key/dtos/request/api-key.create.request.dto'; -import { ApiKeyUpdateDateRequestDto } from 'src/common/api-key/dtos/request/api-key.update-date.request.dto'; -import { ApiKeyUpdateRequestDto } from 'src/common/api-key/dtos/request/api-key.update.request.dto'; -import { ApiKeyCreateResponseDto } from 'src/common/api-key/dtos/response/api-key.create.dto'; -import { ApiKeyGetResponseDto } from 'src/common/api-key/dtos/response/api-key.get.response.dto'; -import { ApiKeyListResponseDto } from 'src/common/api-key/dtos/response/api-key.list.response.dto'; -import { ApiKeyResetResponseDto } from 'src/common/api-key/dtos/response/api-key.reset.dto'; -import { IApiKeyService } from 'src/common/api-key/interfaces/api-key.service.interface'; +} from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { ApiKeyResetResponseDto } from 'src/modules/api-key/dtos/response/api-key.reset.dto'; +import { IApiKeyService } from 'src/modules/api-key/interfaces/api-key.service.interface'; import { ApiKeyDoc, ApiKeyEntity, -} from 'src/common/api-key/repository/entities/api-key.entity'; -import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository'; +} from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyRepository } from 'src/modules/api-key/repository/repositories/api-key.repository'; +import { Document } from 'mongoose'; @Injectable() export class ApiKeyService implements IApiKeyService { @@ -47,33 +49,33 @@ export class ApiKeyService implements IApiKeyService { find?: Record, options?: IDatabaseFindAllOptions ): Promise { - return this.apiKeyRepository.findAll(find, options); + return this.apiKeyRepository.findAll(find, options); } async findOneById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.apiKeyRepository.findOneById(_id, options); + return this.apiKeyRepository.findOneById(_id, options); } async findOne( find: Record, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.apiKeyRepository.findOne(find, options); + return this.apiKeyRepository.findOne(find, options); } async findOneByKey( key: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.apiKeyRepository.findOne({ key }, options); } async findOneByActiveKey( key: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.apiKeyRepository.findOne( { @@ -208,7 +210,7 @@ export class ApiKeyService implements IApiKeyService { repository: ApiKeyDoc, options?: IDatabaseSaveOptions ): Promise { - return this.apiKeyRepository.softDelete(repository, options); + return this.apiKeyRepository.softDelete(repository, null, options); } async validateHashApiKey( @@ -219,16 +221,12 @@ export class ApiKeyService implements IApiKeyService { } async createKey(): Promise { - const random: string = this.helperStringService.random(25, { - safe: false, - }); + const random: string = this.helperStringService.random(25); return `${this.env}_${random}`; } async createSecret(): Promise { - return this.helperStringService.random(35, { - safe: false, - }); + return this.helperStringService.random(35); } async createHashApiKey(key: string, secret: string): Promise { @@ -237,35 +235,57 @@ export class ApiKeyService implements IApiKeyService { async deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise { - return this.apiKeyRepository.deleteMany(find, options); + try { + await this.apiKeyRepository.deleteMany(find, options); + + return true; + } catch (error: unknown) { + throw error; + } } async inactiveManyByEndDate( - options?: IDatabaseManyOptions + options?: IDatabaseUpdateManyOptions ): Promise { - return this.apiKeyRepository.updateMany( - { - endDate: { - $lte: this.helperDateService.create(), + try { + await this.apiKeyRepository.updateMany( + { + endDate: { + $lte: this.helperDateService.create(), + }, + isActive: true, }, - isActive: true, - }, - { - isActive: false, - }, - options - ); - } + { + isActive: false, + }, + options + ); - async mapList(apiKeys: ApiKeyDoc[]): Promise { - const plainObject: ApiKeyEntity[] = apiKeys.map(e => e.toObject()); + return true; + } catch (error: unknown) { + throw error; + } + } - return plainToInstance(ApiKeyListResponseDto, plainObject); + async mapList( + apiKeys: ApiKeyDoc[] | ApiKeyEntity[] + ): Promise { + return plainToInstance( + ApiKeyListResponseDto, + apiKeys.map((e: ApiKeyDoc | ApiKeyEntity) => + e instanceof Document ? e.toObject() : e + ) + ); } - async mapGet(apiKey: ApiKeyDoc): Promise { - return plainToInstance(ApiKeyGetResponseDto, apiKey.toObject()); + async mapGet( + apiKey: ApiKeyDoc | ApiKeyEntity + ): Promise { + return plainToInstance( + ApiKeyGetResponseDto, + apiKey instanceof Document ? apiKey.toObject() : apiKey + ); } } diff --git a/src/common/auth/auth.module.ts b/src/modules/auth/auth.module.ts similarity index 61% rename from src/common/auth/auth.module.ts rename to src/modules/auth/auth.module.ts index 664f53972..197daa02d 100644 --- a/src/common/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,7 +1,7 @@ import { DynamicModule, Module } from '@nestjs/common'; -import { AuthJwtAccessStrategy } from 'src/common/auth/guards/jwt/strategies/auth.jwt.access.strategy'; -import { AuthJwtRefreshStrategy } from 'src/common/auth/guards/jwt/strategies/auth.jwt.refresh.strategy'; -import { AuthService } from 'src/common/auth/services/auth.service'; +import { AuthJwtAccessStrategy } from 'src/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy'; +import { AuthJwtRefreshStrategy } from 'src/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy'; +import { AuthService } from 'src/modules/auth/services/auth.service'; @Module({ providers: [AuthService], diff --git a/src/modules/auth/controllers/auth.admin.controller.ts b/src/modules/auth/controllers/auth.admin.controller.ts new file mode 100644 index 000000000..487b94741 --- /dev/null +++ b/src/modules/auth/controllers/auth.admin.controller.ts @@ -0,0 +1,114 @@ +import { + Controller, + InternalServerErrorException, + Param, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Queue } from 'bullmq'; +import { ClientSession, Connection } from 'mongoose'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; +import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; +import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { AuthAdminUpdatePasswordDoc } from 'src/modules/auth/docs/auth.admin.doc'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; +import { + PolicyAbilityProtected, + PolicyRoleProtected, +} from 'src/modules/policy/decorators/policy.decorator'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { UserNotSelfPipe } from 'src/modules/user/pipes/user.not-self.pipe'; +import { UserParsePipe } from 'src/modules/user/pipes/user.parse.pipe'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; +import { WorkerQueue } from 'src/worker/decorators/worker.decorator'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; + +@ApiTags('modules.admin.auth') +@Controller({ + version: '1', + path: '/auth', +}) +export class AuthAdminController { + constructor( + @DatabaseConnection() private readonly databaseConnection: Connection, + @WorkerQueue(ENUM_WORKER_QUEUES.EMAIL_QUEUE) + private readonly emailQueue: Queue, + private readonly authService: AuthService, + private readonly userService: UserService + ) {} + + @AuthAdminUpdatePasswordDoc() + @Response('auth.updatePassword') + @PolicyAbilityProtected({ + subject: ENUM_POLICY_SUBJECT.AUTH, + action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.UPDATE], + }) + @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Put('/update/:user/password') + async updatePassword( + @Param('user', RequestRequiredPipe, UserParsePipe, UserNotSelfPipe) + user: UserDoc + ): Promise { + const session: ClientSession = + await this.databaseConnection.startSession(); + session.startTransaction(); + + try { + const passwordString = + await this.authService.createPasswordRandom(); + const password = await this.authService.createPassword( + passwordString, + { + temporary: true, + } + ); + user = await this.userService.updatePassword(user, password, { + session, + }); + user = await this.userService.resetPasswordAttempt(user, { + session, + }); + + this.emailQueue.add( + ENUM_EMAIL.TEMP_PASSWORD, + { + email: user.email, + name: user.name, + passwordExpiredAt: password.passwordExpired, + password: passwordString, + }, + { + debounce: { + id: `${ENUM_EMAIL.TEMP_PASSWORD}-${user._id}`, + ttl: 1000, + }, + } + ); + + await session.commitTransaction(); + await session.endSession(); + + return; + } catch (err: any) { + await session.abortTransaction(); + await session.endSession(); + + throw new InternalServerErrorException({ + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } +} diff --git a/src/modules/auth/controllers/auth.public.controller.ts b/src/modules/auth/controllers/auth.public.controller.ts new file mode 100644 index 000000000..b86c82fd0 --- /dev/null +++ b/src/modules/auth/controllers/auth.public.controller.ts @@ -0,0 +1,404 @@ +import { + BadRequestException, + Body, + ConflictException, + Controller, + ForbiddenException, + HttpCode, + HttpStatus, + InternalServerErrorException, + NotFoundException, + Post, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; +import { AuthJwtPayload } from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { + AuthSocialAppleProtected, + AuthSocialGoogleProtected, +} from 'src/modules/auth/decorators/auth.social.decorator'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/enums/role.status-code.enum'; +import { AuthLoginResponseDto } from 'src/modules/auth/dtos/response/auth.login.response.dto'; +import { + AuthPublicLoginCredentialDoc, + AuthPublicLoginSocialAppleDoc, + AuthPublicLoginSocialGoogleDoc, + AuthPublicSignUpDoc, +} from 'src/modules/auth/docs/auth.public.doc'; +import { AuthLoginRequestDto } from 'src/modules/auth/dtos/request/auth.login.request.dto'; +import { UserService } from 'src/modules/user/services/user.service'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; +import { ENUM_USER_STATUS } from 'src/modules/user/enums/user.enum'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; +import { AuthSignUpRequestDto } from 'src/modules/auth/dtos/request/auth.sign-up.request.dto'; +import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/enums/country.status-code.enum'; +import { ClientSession } from 'mongoose'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; +import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; +import { WorkerQueue } from 'src/worker/decorators/worker.decorator'; +import { Connection } from 'mongoose'; +import { Queue } from 'bullmq'; +import { CountryService } from 'src/modules/country/services/country.service'; +import { RoleService } from 'src/modules/role/services/role.service'; + +@ApiTags('modules.public.auth') +@Controller({ + version: '1', + path: '/auth', +}) +export class AuthPublicController { + constructor( + @DatabaseConnection() private readonly databaseConnection: Connection, + @WorkerQueue(ENUM_WORKER_QUEUES.EMAIL_QUEUE) + private readonly emailQueue: Queue, + private readonly userService: UserService, + private readonly authService: AuthService, + private readonly countryService: CountryService, + private readonly roleService: RoleService + ) {} + + @AuthPublicLoginCredentialDoc() + @Response('auth.loginWithCredential') + @ApiKeyProtected() + @HttpCode(HttpStatus.OK) + @Post('/login/credential') + async loginWithCredential( + @Body() { email, password }: AuthLoginRequestDto + ): Promise> { + let user: UserDoc = await this.userService.findOneByEmail(email); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND, + message: 'user.error.notFound', + }); + } + + const passwordAttempt: boolean = + await this.authService.getPasswordAttempt(); + const passwordMaxAttempt: number = + await this.authService.getPasswordMaxAttempt(); + if (passwordAttempt && user.passwordAttempt >= passwordMaxAttempt) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_ATTEMPT_MAX, + message: 'auth.error.passwordAttemptMax', + }); + } + + const validate: boolean = await this.authService.validateUser( + password, + user.password + ); + if (!validate) { + user = await this.userService.increasePasswordAttempt(user); + + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_NOT_MATCH, + message: 'auth.error.passwordNotMatch', + data: { + attempt: user.passwordAttempt, + }, + }); + } else if (user.status !== ENUM_USER_STATUS.ACTIVE) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.join(user); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'role.error.inactive', + }); + } + + await this.userService.resetPasswordAttempt(user); + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + if (checkPasswordExpired) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED, + message: 'auth.error.passwordExpired', + }); + } + + const roleType = userWithRole.role.type; + const tokenType: string = await this.authService.getTokenType(); + + const expiresInAccessToken: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: AuthJwtAccessPayloadDto = + await this.authService.createPayloadAccessToken( + userWithRole, + ENUM_AUTH_LOGIN_FROM.CREDENTIAL + ); + const accessToken: string = await this.authService.createAccessToken( + user.email, + payloadAccessToken + ); + + const payloadRefreshToken: AuthJwtRefreshPayloadDto = + await this.authService.createPayloadRefreshToken( + payloadAccessToken + ); + const refreshToken: string = await this.authService.createRefreshToken( + user.email, + payloadRefreshToken + ); + + return { + data: { + tokenType, + roleType, + expiresIn: expiresInAccessToken, + accessToken, + refreshToken, + }, + }; + } + + @AuthPublicLoginSocialGoogleDoc() + @Response('auth.loginWithSocialGoogle') + @AuthSocialGoogleProtected() + @Post('/login/social/google') + async loginWithGoogle( + @AuthJwtPayload() + { email }: AuthSocialGooglePayloadDto + ): Promise> { + const user: UserDoc = await this.userService.findOneByEmail(email); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND, + message: 'user.error.notFound', + }); + } else if (user.status !== ENUM_USER_STATUS.ACTIVE) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.join(user); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'role.error.inactive', + }); + } + + await this.userService.resetPasswordAttempt(user); + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + if (checkPasswordExpired) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED, + message: 'auth.error.passwordExpired', + }); + } + + const roleType = userWithRole.role.type; + const tokenType: string = await this.authService.getTokenType(); + + const expiresInAccessToken: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: AuthJwtAccessPayloadDto = + await this.authService.createPayloadAccessToken( + userWithRole, + ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE + ); + const accessToken: string = await this.authService.createAccessToken( + user.email, + payloadAccessToken + ); + + const payloadRefreshToken: AuthJwtRefreshPayloadDto = + await this.authService.createPayloadRefreshToken( + payloadAccessToken + ); + const refreshToken: string = await this.authService.createRefreshToken( + user.email, + payloadRefreshToken + ); + + return { + data: { + tokenType, + roleType, + expiresIn: expiresInAccessToken, + accessToken, + refreshToken, + }, + }; + } + + @AuthPublicLoginSocialAppleDoc() + @Response('user.loginWithSocialApple') + @AuthSocialAppleProtected() + @Post('/login/social/apple') + async loginWithApple( + @AuthJwtPayload() + { email }: AuthSocialApplePayloadDto + ): Promise> { + const user: UserDoc = await this.userService.findOneByEmail(email); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND, + message: 'user.error.notFound', + }); + } else if (user.status !== ENUM_USER_STATUS.ACTIVE) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.join(user); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.INACTIVE_FORBIDDEN, + message: 'role.error.inactive', + }); + } + + await this.userService.resetPasswordAttempt(user); + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + if (checkPasswordExpired) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED, + message: 'auth.error.passwordExpired', + }); + } + + const roleType = userWithRole.role.type; + const tokenType: string = await this.authService.getTokenType(); + + const expiresInAccessToken: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: AuthJwtAccessPayloadDto = + await this.authService.createPayloadAccessToken( + userWithRole, + ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE + ); + const accessToken: string = await this.authService.createAccessToken( + user.email, + payloadAccessToken + ); + + const payloadRefreshToken: AuthJwtRefreshPayloadDto = + await this.authService.createPayloadRefreshToken( + payloadAccessToken + ); + const refreshToken: string = await this.authService.createRefreshToken( + user.email, + payloadRefreshToken + ); + + return { + data: { + tokenType, + roleType, + expiresIn: expiresInAccessToken, + accessToken, + refreshToken, + }, + }; + } + + @AuthPublicSignUpDoc() + @Response('auth.signUp') + @ApiKeyProtected() + @Post('/sign-up') + async signUp( + @Body() + { email, name, password: passwordString, country }: AuthSignUpRequestDto + ): Promise { + const promises: Promise[] = [ + this.roleService.findOneByName('user'), + this.userService.existByEmail(email), + this.countryService.findOneActiveById(country), + ]; + + const [role, emailExist, checkCountry] = await Promise.all(promises); + + if (!role) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND, + message: 'role.error.notFound', + }); + } else if (!checkCountry) { + throw new NotFoundException({ + statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'country.error.notFound', + }); + } else if (emailExist) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.EMAIL_EXIST, + message: 'user.error.emailExist', + }); + } + + const password = await this.authService.createPassword(passwordString); + + const session: ClientSession = + await this.databaseConnection.startSession(); + session.startTransaction(); + + try { + const user = await this.userService.signUp( + role._id, + { + email, + name, + password: passwordString, + country, + }, + password, + { session } + ); + + this.emailQueue.add( + ENUM_EMAIL.WELCOME, + { + email, + name, + }, + { + debounce: { + id: `${ENUM_EMAIL.WELCOME}-${user._id}`, + ttl: 1000, + }, + } + ); + + await session.commitTransaction(); + await session.endSession(); + } catch (err: any) { + await session.abortTransaction(); + await session.endSession(); + + throw new InternalServerErrorException({ + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/auth/controllers/auth.shared.controller.ts b/src/modules/auth/controllers/auth.shared.controller.ts new file mode 100644 index 000000000..f8265b1d6 --- /dev/null +++ b/src/modules/auth/controllers/auth.shared.controller.ts @@ -0,0 +1,171 @@ +import { + BadRequestException, + Body, + Controller, + ForbiddenException, + HttpCode, + HttpStatus, + InternalServerErrorException, + Patch, + Post, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ClientSession, Connection } from 'mongoose'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, + AuthJwtRefreshProtected, + AuthJwtToken, +} from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { IAuthPassword } from 'src/modules/auth/interfaces/auth.interface'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthRefreshResponseDto } from 'src/modules/auth/dtos/response/auth.refresh.response.dto'; +import { AuthChangePasswordRequestDto } from 'src/modules/auth/dtos/request/auth.change-password.request.dto'; +import { + AuthSharedChangePasswordDoc, + AuthSharedRefreshDoc, +} from 'src/modules/auth/docs/auth.shared.doc'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; +import { WorkerQueue } from 'src/worker/decorators/worker.decorator'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; +import { Queue } from 'bullmq'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; + +@ApiTags('modules.shared.auth') +@Controller({ + version: '1', + path: '/auth', +}) +export class AuthSharedController { + constructor( + @DatabaseConnection() private readonly databaseConnection: Connection, + @WorkerQueue(ENUM_WORKER_QUEUES.EMAIL_QUEUE) + private readonly emailQueue: Queue, + private readonly userService: UserService, + private readonly authService: AuthService + ) {} + + @AuthSharedRefreshDoc() + @Response('auth.refresh') + @AuthJwtRefreshProtected() + @ApiKeyProtected() + @HttpCode(HttpStatus.OK) + @Post('/refresh') + async refresh( + @AuthJwtToken() refreshToken: string, + @AuthJwtPayload() + { _id, loginFrom }: AuthJwtRefreshPayloadDto + ): Promise> { + const user = await this.userService.findOneActiveById(_id); + + const roleType = user.role.type; + const tokenType: string = await this.authService.getTokenType(); + + const expiresInAccessToken: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: AuthJwtAccessPayloadDto = + await this.authService.createPayloadAccessToken(user, loginFrom); + const accessToken: string = await this.authService.createAccessToken( + user.email, + payloadAccessToken + ); + + return { + data: { + tokenType, + roleType, + expiresIn: expiresInAccessToken, + accessToken, + refreshToken, + }, + }; + } + + @AuthSharedChangePasswordDoc() + @Response('auth.changePassword') + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Patch('/change-password') + async changePassword( + @Body() body: AuthChangePasswordRequestDto, + @AuthJwtPayload() + { _id }: AuthJwtAccessPayloadDto + ): Promise { + let user = await this.userService.findOneById(_id); + + const passwordAttempt: boolean = + await this.authService.getPasswordAttempt(); + const passwordMaxAttempt: number = + await this.authService.getPasswordMaxAttempt(); + if (passwordAttempt && user.passwordAttempt >= passwordMaxAttempt) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_ATTEMPT_MAX, + message: 'auth.error.passwordAttemptMax', + }); + } + + const matchPassword: boolean = await this.authService.validateUser( + body.oldPassword, + user.password + ); + if (!matchPassword) { + await this.userService.increasePasswordAttempt(user); + + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_NOT_MATCH, + message: 'auth.error.passwordNotMatch', + }); + } + + const password: IAuthPassword = await this.authService.createPassword( + body.newPassword + ); + + const session: ClientSession = + await this.databaseConnection.startSession(); + session.startTransaction(); + + try { + user = await this.userService.resetPasswordAttempt(user, { + session, + }); + user = await this.userService.updatePassword(user, password, { + session, + }); + + this.emailQueue.add( + ENUM_EMAIL.CHANGE_PASSWORD, + { + email: user.email, + name: user.name, + }, + { + debounce: { + id: `${ENUM_EMAIL.CHANGE_PASSWORD}-${user._id}`, + ttl: 1000, + }, + } + ); + + await session.commitTransaction(); + await session.endSession(); + } catch (err: any) { + await session.abortTransaction(); + await session.endSession(); + + throw new InternalServerErrorException({ + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } +} diff --git a/src/common/auth/decorators/auth.jwt.decorator.ts b/src/modules/auth/decorators/auth.jwt.decorator.ts similarity index 80% rename from src/common/auth/decorators/auth.jwt.decorator.ts rename to src/modules/auth/decorators/auth.jwt.decorator.ts index 731b36dd3..384419ec5 100644 --- a/src/common/auth/decorators/auth.jwt.decorator.ts +++ b/src/modules/auth/decorators/auth.jwt.decorator.ts @@ -1,9 +1,9 @@ import { applyDecorators, UseGuards } from '@nestjs/common'; import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; -import { AuthJwtAccessGuard } from 'src/common/auth/guards/jwt/auth.jwt.access.guard'; -import { AuthJwtRefreshGuard } from 'src/common/auth/guards/jwt/auth.jwt.refresh.guard'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtAccessGuard } from 'src/modules/auth/guards/jwt/auth.jwt.access.guard'; +import { AuthJwtRefreshGuard } from 'src/modules/auth/guards/jwt/auth.jwt.refresh.guard'; export const AuthJwtPayload = createParamDecorator( (data: string, ctx: ExecutionContext): T => { diff --git a/src/common/auth/decorators/auth.social.decorator.ts b/src/modules/auth/decorators/auth.social.decorator.ts similarity index 62% rename from src/common/auth/decorators/auth.social.decorator.ts rename to src/modules/auth/decorators/auth.social.decorator.ts index 605f37aae..1b9f693b8 100644 --- a/src/common/auth/decorators/auth.social.decorator.ts +++ b/src/modules/auth/decorators/auth.social.decorator.ts @@ -1,6 +1,6 @@ import { UseGuards, applyDecorators } from '@nestjs/common'; -import { AuthSocialAppleGuard } from 'src/common/auth/guards/social/auth.social.apple.guard'; -import { AuthSocialGoogleGuard } from 'src/common/auth/guards/social/auth.social.google.guard'; +import { AuthSocialAppleGuard } from 'src/modules/auth/guards/social/auth.social.apple.guard'; +import { AuthSocialGoogleGuard } from 'src/modules/auth/guards/social/auth.social.google.guard'; export function AuthSocialGoogleProtected(): MethodDecorator { return applyDecorators(UseGuards(AuthSocialGoogleGuard)); diff --git a/src/modules/auth/docs/auth.admin.doc.ts b/src/modules/auth/docs/auth.admin.doc.ts new file mode 100644 index 000000000..616023d0f --- /dev/null +++ b/src/modules/auth/docs/auth.admin.doc.ts @@ -0,0 +1,26 @@ +import { applyDecorators } from '@nestjs/common'; +import { + Doc, + DocAuth, + DocGuard, + DocRequest, + DocResponse, +} from 'src/common/doc/decorators/doc.decorator'; +import { UserDocParamsId } from 'src/modules/user/constants/user.doc.constant'; + +export function AuthAdminUpdatePasswordDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'admin update user password', + }), + DocRequest({ + params: UserDocParamsId, + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocGuard({ role: true, policy: true }), + DocResponse('auth.updatePassword') + ); +} diff --git a/src/modules/auth/docs/auth.public.doc.ts b/src/modules/auth/docs/auth.public.doc.ts new file mode 100644 index 000000000..368ea371c --- /dev/null +++ b/src/modules/auth/docs/auth.public.doc.ts @@ -0,0 +1,69 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + Doc, + DocAuth, + DocRequest, + DocResponse, +} from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { AuthLoginRequestDto } from 'src/modules/auth/dtos/request/auth.login.request.dto'; +import { AuthSignUpRequestDto } from 'src/modules/auth/dtos/request/auth.sign-up.request.dto'; +import { AuthLoginResponseDto } from 'src/modules/auth/dtos/response/auth.login.response.dto'; + +export function AuthPublicLoginCredentialDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'Login with email and password', + }), + DocAuth({ xApiKey: true }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: AuthLoginRequestDto, + }), + DocResponse('auth.loginWithCredential', { + dto: AuthLoginResponseDto, + }) + ); +} + +export function AuthPublicLoginSocialGoogleDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'Login with social google', + }), + DocAuth({ xApiKey: true, google: true }), + DocResponse('auth.loginWithSocialGoogle', { + dto: AuthLoginResponseDto, + }) + ); +} + +export function AuthPublicLoginSocialAppleDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'Login with social apple', + }), + DocAuth({ xApiKey: true, apple: true }), + DocResponse('auth.loginWithSocialApple', { + dto: AuthLoginResponseDto, + }) + ); +} + +export function AuthPublicSignUpDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'Sign up', + }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: AuthSignUpRequestDto, + }), + DocAuth({ + xApiKey: true, + }), + DocResponse('auth.signUp', { + httpStatus: HttpStatus.CREATED, + }) + ); +} diff --git a/src/modules/auth/docs/auth.shared.doc.ts b/src/modules/auth/docs/auth.shared.doc.ts new file mode 100644 index 000000000..ab1a49428 --- /dev/null +++ b/src/modules/auth/docs/auth.shared.doc.ts @@ -0,0 +1,42 @@ +import { applyDecorators } from '@nestjs/common'; +import { + Doc, + DocAuth, + DocRequest, + DocResponse, +} from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { AuthChangePasswordRequestDto } from 'src/modules/auth/dtos/request/auth.change-password.request.dto'; +import { AuthRefreshResponseDto } from 'src/modules/auth/dtos/response/auth.refresh.response.dto'; + +export function AuthSharedRefreshDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'refresh a token', + }), + DocAuth({ + xApiKey: true, + jwtRefreshToken: true, + }), + DocResponse('auth.refresh', { + dto: AuthRefreshResponseDto, + }) + ); +} + +export function AuthSharedChangePasswordDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'change password', + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: AuthChangePasswordRequestDto, + }), + DocResponse('auth.changePassword') + ); +} diff --git a/src/common/auth/dtos/jwt/auth.jwt.access-payload.dto.ts b/src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.ts similarity index 85% rename from src/common/auth/dtos/jwt/auth.jwt.access-payload.dto.ts rename to src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.ts index b18203465..f4f0a4cdd 100644 --- a/src/common/auth/dtos/jwt/auth.jwt.access-payload.dto.ts +++ b/src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.ts @@ -1,13 +1,13 @@ import { faker } from '@faker-js/faker'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { ENUM_AUTH_LOGIN_FROM } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; import { ENUM_POLICY_ACTION, ENUM_POLICY_REQUEST_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; export class AuthJwtAccessPayloadPermissionDto { @ApiProperty({ @@ -52,39 +52,39 @@ export class AuthJwtAccessPayloadDto { nullable: false, example: faker.date.recent(), }) - readonly loginDate: Date; + loginDate: Date; @ApiProperty({ required: true, nullable: false, enum: ENUM_AUTH_LOGIN_FROM, }) - readonly loginFrom: ENUM_AUTH_LOGIN_FROM; + loginFrom: ENUM_AUTH_LOGIN_FROM; @ApiProperty({ required: true, nullable: false, }) - readonly _id: string; + _id: string; @ApiProperty({ required: true, nullable: false, }) - readonly email: string; + email: string; @ApiProperty({ required: true, nullable: false, }) - readonly role: string; + role: string; @ApiProperty({ required: true, nullable: false, enum: ENUM_POLICY_ROLE_TYPE, }) - readonly type: ENUM_POLICY_ROLE_TYPE; + type: ENUM_POLICY_ROLE_TYPE; @ApiProperty({ required: true, @@ -94,5 +94,5 @@ export class AuthJwtAccessPayloadDto { default: [], }) @Type(() => AuthJwtAccessPayloadPermissionDto) - readonly permissions: AuthJwtAccessPayloadPermissionDto[]; + permissions: AuthJwtAccessPayloadPermissionDto[]; } diff --git a/src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts b/src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts similarity index 66% rename from src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts rename to src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts index cb0116abf..41b40603f 100644 --- a/src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts +++ b/src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; export class AuthJwtRefreshPayloadDto extends OmitType( AuthJwtAccessPayloadDto, diff --git a/src/modules/user/dtos/request/user.change-password.request.dto.ts b/src/modules/auth/dtos/request/auth.change-password.request.dto.ts similarity index 89% rename from src/modules/user/dtos/request/user.change-password.request.dto.ts rename to src/modules/auth/dtos/request/auth.change-password.request.dto.ts index d692a8a83..6fdc4a455 100644 --- a/src/modules/user/dtos/request/user.change-password.request.dto.ts +++ b/src/modules/auth/dtos/request/auth.change-password.request.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsNotEmpty, MaxLength, MinLength } from 'class-validator'; import { IsPassword } from 'src/common/request/validations/request.is-password.validation'; -export class UserChangePasswordRequestDto { +export class AuthChangePasswordRequestDto { @ApiProperty({ description: "new string password, newPassword can't same with oldPassword", @@ -19,7 +19,7 @@ export class UserChangePasswordRequestDto { @IsPassword() @MinLength(8) @MaxLength(50) - readonly newPassword: string; + newPassword: string; @ApiProperty({ description: 'old string password', @@ -30,5 +30,5 @@ export class UserChangePasswordRequestDto { }) @IsString() @IsNotEmpty() - readonly oldPassword: string; + oldPassword: string; } diff --git a/src/modules/user/dtos/request/user.login.request.dto.ts b/src/modules/auth/dtos/request/auth.login.request.dto.ts similarity index 73% rename from src/modules/user/dtos/request/user.login.request.dto.ts rename to src/modules/auth/dtos/request/auth.login.request.dto.ts index cce135021..0df2d8e68 100644 --- a/src/modules/user/dtos/request/user.login.request.dto.ts +++ b/src/modules/auth/dtos/request/auth.login.request.dto.ts @@ -1,8 +1,8 @@ import { faker } from '@faker-js/faker'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; -export class UserLoginRequestDto { +export class AuthLoginRequestDto { @ApiProperty({ required: true, nullable: false, @@ -10,7 +10,8 @@ export class UserLoginRequestDto { }) @IsString() @IsNotEmpty() - readonly email: string; + @IsEmail() + email: string; @ApiProperty({ description: 'string password', @@ -20,5 +21,5 @@ export class UserLoginRequestDto { }) @IsString() @IsNotEmpty() - readonly password: string; + password: string; } diff --git a/src/modules/user/dtos/request/user.sign-up.request.dto.ts b/src/modules/auth/dtos/request/auth.sign-up.request.dto.ts similarity index 88% rename from src/modules/user/dtos/request/user.sign-up.request.dto.ts rename to src/modules/auth/dtos/request/auth.sign-up.request.dto.ts index bc072dafd..5ead06980 100644 --- a/src/modules/user/dtos/request/user.sign-up.request.dto.ts +++ b/src/modules/auth/dtos/request/auth.sign-up.request.dto.ts @@ -4,7 +4,7 @@ import { IsNotEmpty, MaxLength, MinLength } from 'class-validator'; import { IsPassword } from 'src/common/request/validations/request.is-password.validation'; import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; -export class UserSignUpRequestDto extends OmitType(UserCreateRequestDto, [ +export class AuthSignUpRequestDto extends OmitType(UserCreateRequestDto, [ 'role', ] as const) { @ApiProperty({ @@ -20,5 +20,5 @@ export class UserSignUpRequestDto extends OmitType(UserCreateRequestDto, [ @IsPassword() @MinLength(8) @MaxLength(50) - readonly password: string; + password: string; } diff --git a/src/modules/user/dtos/response/user.login.response.dto.ts b/src/modules/auth/dtos/response/auth.login.response.dto.ts similarity index 66% rename from src/modules/user/dtos/response/user.login.response.dto.ts rename to src/modules/auth/dtos/response/auth.login.response.dto.ts index 0b9a1a67b..a01b8aaf9 100644 --- a/src/modules/user/dtos/response/user.login.response.dto.ts +++ b/src/modules/auth/dtos/response/auth.login.response.dto.ts @@ -1,13 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; -export class UserLoginResponseDto { +export class AuthLoginResponseDto { @ApiProperty({ example: 'Bearer', required: true, nullable: false, }) - readonly tokenType: string; + tokenType: string; @ApiProperty({ example: ENUM_POLICY_ROLE_TYPE.USER, @@ -15,7 +15,7 @@ export class UserLoginResponseDto { required: true, nullable: false, }) - readonly roleType: ENUM_POLICY_ROLE_TYPE; + roleType: ENUM_POLICY_ROLE_TYPE; @ApiProperty({ example: 3600, @@ -23,17 +23,17 @@ export class UserLoginResponseDto { required: true, nullable: false, }) - readonly expiresIn: number; + expiresIn: number; @ApiProperty({ required: true, nullable: false, }) - readonly accessToken: string; + accessToken: string; @ApiProperty({ required: true, nullable: false, }) - readonly refreshToken: string; + refreshToken: string; } diff --git a/src/modules/auth/dtos/response/auth.refresh.response.dto.ts b/src/modules/auth/dtos/response/auth.refresh.response.dto.ts new file mode 100644 index 000000000..f1ddd5b66 --- /dev/null +++ b/src/modules/auth/dtos/response/auth.refresh.response.dto.ts @@ -0,0 +1,3 @@ +import { AuthLoginResponseDto } from 'src/modules/auth/dtos/response/auth.login.response.dto'; + +export class AuthRefreshResponseDto extends AuthLoginResponseDto {} diff --git a/src/modules/auth/dtos/social/auth.social.apple-payload.dto.ts b/src/modules/auth/dtos/social/auth.social.apple-payload.dto.ts new file mode 100644 index 000000000..2191e80e8 --- /dev/null +++ b/src/modules/auth/dtos/social/auth.social.apple-payload.dto.ts @@ -0,0 +1,3 @@ +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; + +export class AuthSocialApplePayloadDto extends AuthSocialGooglePayloadDto {} diff --git a/src/common/auth/dtos/social/auth.social.google-payload.dto.ts b/src/modules/auth/dtos/social/auth.social.google-payload.dto.ts similarity index 100% rename from src/common/auth/dtos/social/auth.social.google-payload.dto.ts rename to src/modules/auth/dtos/social/auth.social.google-payload.dto.ts diff --git a/src/common/auth/constants/auth.enum.constant.ts b/src/modules/auth/enums/auth.enum.ts similarity index 100% rename from src/common/auth/constants/auth.enum.constant.ts rename to src/modules/auth/enums/auth.enum.ts diff --git a/src/modules/auth/enums/auth.status-code.enum.ts b/src/modules/auth/enums/auth.status-code.enum.ts new file mode 100644 index 000000000..5881f2003 --- /dev/null +++ b/src/modules/auth/enums/auth.status-code.enum.ts @@ -0,0 +1,6 @@ +export enum ENUM_AUTH_STATUS_CODE_ERROR { + JWT_ACCESS_TOKEN = 5000, + JWT_REFRESH_TOKEN = 5001, + SOCIAL_GOOGLE = 5002, + SOCIAL_APPLE = 5003, +} diff --git a/src/common/auth/guards/jwt/auth.jwt.access.guard.ts b/src/modules/auth/guards/jwt/auth.jwt.access.guard.ts similarity index 83% rename from src/common/auth/guards/jwt/auth.jwt.access.guard.ts rename to src/modules/auth/guards/jwt/auth.jwt.access.guard.ts index 3db97fd43..a87beb219 100644 --- a/src/common/auth/guards/jwt/auth.jwt.access.guard.ts +++ b/src/modules/auth/guards/jwt/auth.jwt.access.guard.ts @@ -1,13 +1,13 @@ import { AuthGuard } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; @Injectable() export class AuthJwtAccessGuard extends AuthGuard('jwtAccess') { handleRequest(err: Error, user: TUser, info: Error): TUser { if (err || !user) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN, message: 'auth.error.accessTokenUnauthorized', _error: err ? err.message : info.message, }); diff --git a/src/common/auth/guards/jwt/auth.jwt.refresh.guard.ts b/src/modules/auth/guards/jwt/auth.jwt.refresh.guard.ts similarity index 83% rename from src/common/auth/guards/jwt/auth.jwt.refresh.guard.ts rename to src/modules/auth/guards/jwt/auth.jwt.refresh.guard.ts index 8202613f6..ec728a97d 100644 --- a/src/common/auth/guards/jwt/auth.jwt.refresh.guard.ts +++ b/src/modules/auth/guards/jwt/auth.jwt.refresh.guard.ts @@ -1,13 +1,13 @@ import { AuthGuard } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; @Injectable() export class AuthJwtRefreshGuard extends AuthGuard('jwtRefresh') { handleRequest(err: Error, user: TUser, info: Error): TUser { if (err || !user) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN, message: 'auth.error.refreshTokenUnauthorized', _error: err ? err.message : info.message, }); diff --git a/src/common/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts b/src/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts similarity index 86% rename from src/common/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts rename to src/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts index 0d1c2df41..754696e81 100644 --- a/src/common/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts +++ b/src/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.ts @@ -2,7 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; @Injectable() export class AuthJwtAccessStrategy extends PassportStrategy( @@ -19,7 +19,6 @@ export class AuthJwtAccessStrategy extends PassportStrategy( ignoreNotBefore: true, audience: configService.get('auth.jwt.audience'), issuer: configService.get('auth.jwt.issuer'), - subject: configService.get('auth.jwt.subject'), }, secretOrKey: configService.get( 'auth.jwt.accessToken.secretKey' diff --git a/src/common/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts b/src/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts similarity index 86% rename from src/common/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts rename to src/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts index 6ffe96291..a166bb26d 100644 --- a/src/common/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts +++ b/src/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.ts @@ -2,7 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { AuthJwtRefreshPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; @Injectable() export class AuthJwtRefreshStrategy extends PassportStrategy( @@ -19,7 +19,6 @@ export class AuthJwtRefreshStrategy extends PassportStrategy( ignoreNotBefore: false, audience: configService.get('auth.jwt.audience'), issuer: configService.get('auth.jwt.issuer'), - subject: configService.get('auth.jwt.subject'), }, secretOrKey: configService.get( 'auth.jwt.refreshToken.secretKey' diff --git a/src/common/auth/guards/social/auth.social.apple.guard.ts b/src/modules/auth/guards/social/auth.social.apple.guard.ts similarity index 81% rename from src/common/auth/guards/social/auth.social.apple.guard.ts rename to src/modules/auth/guards/social/auth.social.apple.guard.ts index 54b55ab22..e0a7684ec 100644 --- a/src/common/auth/guards/social/auth.social.apple.guard.ts +++ b/src/modules/auth/guards/social/auth.social.apple.guard.ts @@ -5,9 +5,9 @@ import { UnauthorizedException, } from '@nestjs/common'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; -import { AuthSocialApplePayloadDto } from 'src/common/auth/dtos/social/auth.social.apple-payload.dto'; -import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; +import { AuthService } from 'src/modules/auth/services/auth.service'; @Injectable() export class AuthSocialAppleGuard implements CanActivate { @@ -22,7 +22,7 @@ export class AuthSocialAppleGuard implements CanActivate { if (acArr.length !== 2) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, message: 'auth.error.socialApple', }); } @@ -40,8 +40,9 @@ export class AuthSocialAppleGuard implements CanActivate { return true; } catch (err: any) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, message: 'auth.error.socialApple', + _error: err.message, }); } } diff --git a/src/common/auth/guards/social/auth.social.google.guard.ts b/src/modules/auth/guards/social/auth.social.google.guard.ts similarity index 81% rename from src/common/auth/guards/social/auth.social.google.guard.ts rename to src/modules/auth/guards/social/auth.social.google.guard.ts index 8da7ffb5f..9d398327b 100644 --- a/src/common/auth/guards/social/auth.social.google.guard.ts +++ b/src/modules/auth/guards/social/auth.social.google.guard.ts @@ -5,9 +5,9 @@ import { UnauthorizedException, } from '@nestjs/common'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; -import { AuthSocialGooglePayloadDto } from 'src/common/auth/dtos/social/auth.social.google-payload.dto'; -import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; +import { AuthService } from 'src/modules/auth/services/auth.service'; @Injectable() export class AuthSocialGoogleGuard implements CanActivate { @@ -21,7 +21,7 @@ export class AuthSocialGoogleGuard implements CanActivate { const acArr = authorization?.split('Bearer ') ?? []; if (acArr.length !== 2) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, message: 'auth.error.socialGoogle', }); } @@ -39,8 +39,9 @@ export class AuthSocialGoogleGuard implements CanActivate { return true; } catch (err: any) { throw new UnauthorizedException({ - statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE_ERROR, + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, message: 'auth.error.socialGoogle', + _error: err.message, }); } } diff --git a/src/common/auth/interfaces/auth.interface.ts b/src/modules/auth/interfaces/auth.interface.ts similarity index 66% rename from src/common/auth/interfaces/auth.interface.ts rename to src/modules/auth/interfaces/auth.interface.ts index dda232e2d..59e1f60fd 100644 --- a/src/common/auth/interfaces/auth.interface.ts +++ b/src/modules/auth/interfaces/auth.interface.ts @@ -4,3 +4,7 @@ export interface IAuthPassword { passwordExpired: Date; passwordCreated: Date; } + +export interface IAuthPasswordOptions { + temporary: boolean; +} diff --git a/src/common/auth/interfaces/auth.service.interface.ts b/src/modules/auth/interfaces/auth.service.interface.ts similarity index 53% rename from src/common/auth/interfaces/auth.service.interface.ts rename to src/modules/auth/interfaces/auth.service.interface.ts index 1f9148a14..c67fef379 100644 --- a/src/common/auth/interfaces/auth.service.interface.ts +++ b/src/modules/auth/interfaces/auth.service.interface.ts @@ -1,17 +1,26 @@ import { Document } from 'mongoose'; -import { ENUM_AUTH_LOGIN_FROM } from 'src/common/auth/constants/auth.enum.constant'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; -import { AuthJwtRefreshPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; -import { AuthSocialApplePayloadDto } from 'src/common/auth/dtos/social/auth.social.apple-payload.dto'; -import { AuthSocialGooglePayloadDto } from 'src/common/auth/dtos/social/auth.social.google-payload.dto'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; +import { + IAuthPassword, + IAuthPasswordOptions, +} from 'src/modules/auth/interfaces/auth.interface'; export interface IAuthService { - createAccessToken(payload: AuthJwtAccessPayloadDto): Promise; - validateAccessToken(token: string): Promise; + createAccessToken( + subject: string, + payload: AuthJwtAccessPayloadDto + ): Promise; + validateAccessToken(subject: string, token: string): Promise; payloadAccessToken(token: string): Promise; - createRefreshToken(payload: AuthJwtRefreshPayloadDto): Promise; - validateRefreshToken(token: string): Promise; + createRefreshToken( + subject: string, + payload: AuthJwtRefreshPayloadDto + ): Promise; + validateRefreshToken(subject: string, token: string): Promise; payloadRefreshToken(token: string): Promise; validateUser( passwordString: string, @@ -27,7 +36,10 @@ export interface IAuthService { loginDate, }: AuthJwtAccessPayloadDto): Promise; createSalt(length: number): Promise; - createPassword(password: string): Promise; + createPassword( + password: string, + options?: IAuthPasswordOptions + ): Promise; createPasswordRandom(): Promise; checkPasswordExpired(passwordExpired: Date): Promise; getTokenType(): Promise; @@ -35,7 +47,6 @@ export interface IAuthService { getRefreshTokenExpirationTime(): Promise; getIssuer(): Promise; getAudience(): Promise; - getSubject(): Promise; getPasswordAttempt(): Promise; getPasswordMaxAttempt(): Promise; appleGetTokenInfo(code: string): Promise; diff --git a/src/common/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts similarity index 83% rename from src/common/auth/services/auth.service.ts rename to src/modules/auth/services/auth.service.ts index 133d157cf..eaca8eda5 100644 --- a/src/common/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -6,13 +6,16 @@ import { HelperDateService } from 'src/common/helper/services/helper.date.servic import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; import { HelperStringService } from 'src/common/helper/services/helper.string.service'; -import { IAuthService } from 'src/common/auth/interfaces/auth.service.interface'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; -import { AuthJwtRefreshPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { AuthSocialApplePayloadDto } from 'src/common/auth/dtos/social/auth.social.apple-payload.dto'; -import { AuthSocialGooglePayloadDto } from 'src/common/auth/dtos/social/auth.social.google-payload.dto'; -import { ENUM_AUTH_LOGIN_FROM } from 'src/common/auth/constants/auth.enum.constant'; +import { IAuthService } from 'src/modules/auth/interfaces/auth.service.interface'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { + IAuthPassword, + IAuthPasswordOptions, +} from 'src/modules/auth/interfaces/auth.interface'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; import { plainToInstance } from 'class-transformer'; import { Document } from 'mongoose'; @@ -28,10 +31,10 @@ export class AuthService implements IAuthService { private readonly jwtPrefixAuthorization: string; private readonly jwtAudience: string; private readonly jwtIssuer: string; - private readonly jwtSubject: string; // password private readonly passwordExpiredIn: number; + private readonly passwordExpiredTemporary: number; private readonly passwordSaltLength: number; private readonly passwordAttempt: boolean; @@ -69,7 +72,6 @@ export class AuthService implements IAuthService { this.jwtPrefixAuthorization = this.configService.get( 'auth.jwt.prefixAuthorization' ); - this.jwtSubject = this.configService.get('auth.jwt.subject'); this.jwtAudience = this.configService.get('auth.jwt.audience'); this.jwtIssuer = this.configService.get('auth.jwt.issuer'); @@ -77,9 +79,13 @@ export class AuthService implements IAuthService { this.passwordExpiredIn = this.configService.get( 'auth.password.expiredIn' ); + this.passwordExpiredTemporary = this.configService.get( + 'auth.password.expiredInTemporary' + ); this.passwordSaltLength = this.configService.get( 'auth.password.saltLength' ); + this.passwordAttempt = this.configService.get( 'auth.password.attempt' ); @@ -102,7 +108,10 @@ export class AuthService implements IAuthService { ); } - async createAccessToken(payload: AuthJwtAccessPayloadDto): Promise { + async createAccessToken( + subject: string, + payload: AuthJwtAccessPayloadDto + ): Promise { return this.helperEncryptionService.jwtEncrypt( { ...payload }, { @@ -110,17 +119,20 @@ export class AuthService implements IAuthService { expiredIn: this.jwtAccessTokenExpirationTime, audience: this.jwtAudience, issuer: this.jwtIssuer, - subject: this.jwtSubject, + subject, } ); } - async validateAccessToken(token: string): Promise { + async validateAccessToken( + subject: string, + token: string + ): Promise { return this.helperEncryptionService.jwtVerify(token, { secretKey: this.jwtAccessTokenSecretKey, audience: this.jwtAudience, issuer: this.jwtIssuer, - subject: this.jwtSubject, + subject, }); } @@ -131,6 +143,7 @@ export class AuthService implements IAuthService { } async createRefreshToken( + subject: string, payload: AuthJwtRefreshPayloadDto ): Promise { return this.helperEncryptionService.jwtEncrypt( @@ -140,17 +153,20 @@ export class AuthService implements IAuthService { expiredIn: this.jwtRefreshTokenExpirationTime, audience: this.jwtAudience, issuer: this.jwtIssuer, - subject: this.jwtSubject, + subject, } ); } - async validateRefreshToken(token: string): Promise { + async validateRefreshToken( + subject: string, + token: string + ): Promise { return this.helperEncryptionService.jwtVerify(token, { secretKey: this.jwtRefreshTokenSecretKey, audience: this.jwtAudience, issuer: this.jwtIssuer, - subject: this.jwtSubject, + subject, }); } @@ -206,11 +222,16 @@ export class AuthService implements IAuthService { return this.helperHashService.randomSalt(length); } - async createPassword(password: string): Promise { + async createPassword( + password: string, + options?: IAuthPasswordOptions + ): Promise { const salt: string = await this.createSalt(this.passwordSaltLength); const passwordExpired: Date = this.helperDateService.forwardInSeconds( - this.passwordExpiredIn + options?.temporary + ? this.passwordExpiredTemporary + : this.passwordExpiredIn ); const passwordCreated: Date = this.helperDateService.create(); const passwordHash = this.helperHashService.bcrypt(password, salt); @@ -254,10 +275,6 @@ export class AuthService implements IAuthService { return this.jwtAudience; } - async getSubject(): Promise { - return this.jwtSubject; - } - async getPasswordAttempt(): Promise { return this.passwordAttempt; } @@ -280,15 +297,11 @@ export class AuthService implements IAuthService { async googleGetTokenInfo( idToken: string ): Promise { - try { - const login: LoginTicket = await this.googleClient.verifyIdToken({ - idToken: idToken, - }); - const payload = login.getPayload(); - - return { email: payload.email }; - } catch (err) { - throw err; - } + const login: LoginTicket = await this.googleClient.verifyIdToken({ + idToken: idToken, + }); + const payload = login.getPayload(); + + return { email: payload.email }; } } diff --git a/src/common/aws/aws.module.ts b/src/modules/aws/aws.module.ts similarity index 58% rename from src/common/aws/aws.module.ts rename to src/modules/aws/aws.module.ts index 9042c983a..94a415c66 100644 --- a/src/common/aws/aws.module.ts +++ b/src/modules/aws/aws.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; -import { AwsSESService } from 'src/common/aws/services/aws.ses.service'; +import { AwsS3Service } from 'src/modules/aws/services/aws.s3.service'; +import { AwsSESService } from 'src/modules/aws/services/aws.ses.service'; @Module({ exports: [AwsS3Service, AwsSESService], diff --git a/src/common/aws/constants/aws.constant.ts b/src/modules/aws/constants/aws.constant.ts similarity index 100% rename from src/common/aws/constants/aws.constant.ts rename to src/modules/aws/constants/aws.constant.ts diff --git a/src/common/aws/dtos/aws.s3-multipart.dto.ts b/src/modules/aws/dtos/aws.s3-multipart.dto.ts similarity index 96% rename from src/common/aws/dtos/aws.s3-multipart.dto.ts rename to src/modules/aws/dtos/aws.s3-multipart.dto.ts index 2c7ddf2b9..12258dfaf 100644 --- a/src/common/aws/dtos/aws.s3-multipart.dto.ts +++ b/src/modules/aws/dtos/aws.s3-multipart.dto.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; export class AwsS3MultipartPartDto { @ApiProperty({ diff --git a/src/modules/aws/dtos/aws.s3-presign-url.dto.ts b/src/modules/aws/dtos/aws.s3-presign-url.dto.ts new file mode 100644 index 000000000..75faccbc7 --- /dev/null +++ b/src/modules/aws/dtos/aws.s3-presign-url.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; + +export class AwsS3PresignUrlDto extends AwsS3Dto { + @ApiProperty({ + required: true, + nullable: false, + example: 10000, + description: 'Expired in millisecond for each presign url', + }) + expiredIn: number; +} diff --git a/src/common/aws/dtos/aws.s3.dto.ts b/src/modules/aws/dtos/aws.s3.dto.ts similarity index 100% rename from src/common/aws/dtos/aws.s3.dto.ts rename to src/modules/aws/dtos/aws.s3.dto.ts diff --git a/src/common/aws/dtos/aws.ses.dto.ts b/src/modules/aws/dtos/aws.ses.dto.ts similarity index 98% rename from src/common/aws/dtos/aws.ses.dto.ts rename to src/modules/aws/dtos/aws.ses.dto.ts index 47d3f7fc9..897f5d41c 100644 --- a/src/common/aws/dtos/aws.ses.dto.ts +++ b/src/modules/aws/dtos/aws.ses.dto.ts @@ -132,7 +132,7 @@ export class AwsSESSendBulkDto extends OmitType(AwsSESSendDto, [ @ApiProperty({ required: true, isArray: true, - type: () => AwsSESSendBulkRecipientsDto, + type: AwsSESSendBulkRecipientsDto, }) @IsNotEmpty() @IsArray() diff --git a/src/common/aws/interfaces/aws.interface.ts b/src/modules/aws/interfaces/aws.interface.ts similarity index 68% rename from src/common/aws/interfaces/aws.interface.ts rename to src/modules/aws/interfaces/aws.interface.ts index 0bd65e6b5..3daf500a1 100644 --- a/src/common/aws/interfaces/aws.interface.ts +++ b/src/modules/aws/interfaces/aws.interface.ts @@ -9,9 +9,8 @@ export interface IAwsS3PutItemWithAclOptions extends IAwsS3PutItemOptions { acl?: ObjectCannedACL; } -export interface IAwsS3RandomFilename { - path: string; - customFilename: string; +export interface IAwsS3PutPresignUrlOptions { + path?: string; } export interface IAwsS3PutItem { @@ -19,3 +18,9 @@ export interface IAwsS3PutItem { originalname: string; size: number; } + +export interface IAwsS3PutPresignUrlFile { + filename: string; + size: number; + duration?: number; +} diff --git a/src/common/aws/interfaces/aws.s3-service.interface.ts b/src/modules/aws/interfaces/aws.s3-service.interface.ts similarity index 79% rename from src/common/aws/interfaces/aws.s3-service.interface.ts rename to src/modules/aws/interfaces/aws.s3-service.interface.ts index 30cf3857b..95f7823af 100644 --- a/src/common/aws/interfaces/aws.s3-service.interface.ts +++ b/src/modules/aws/interfaces/aws.s3-service.interface.ts @@ -2,13 +2,16 @@ import { HeadBucketCommandOutput, UploadPartRequest } from '@aws-sdk/client-s3'; import { AwsS3MultipartDto, AwsS3MultipartPartDto, -} from 'src/common/aws/dtos/aws.s3-multipart.dto'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; +} from 'src/modules/aws/dtos/aws.s3-multipart.dto'; +import { AwsS3PresignUrlDto } from 'src/modules/aws/dtos/aws.s3-presign-url.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; import { IAwsS3PutItem, IAwsS3PutItemOptions, IAwsS3PutItemWithAclOptions, -} from 'src/common/aws/interfaces/aws.interface'; + IAwsS3PutPresignUrlFile, + IAwsS3PutPresignUrlOptions, +} from 'src/modules/aws/interfaces/aws.interface'; import { Readable } from 'stream'; export interface IAwsS3Service { @@ -50,6 +53,8 @@ export interface IAwsS3Service { ): Promise; completeMultipart(multipart: AwsS3MultipartDto): Promise; abortMultipart(multipart: AwsS3MultipartDto): Promise; - getFilenameFromCompletedUrl(completedUrl: string): Promise; - createRandomFilename(path?: string): Promise>; + setPresignUrl( + { filename, size, duration }: IAwsS3PutPresignUrlFile, + options?: IAwsS3PutPresignUrlOptions + ): Promise; } diff --git a/src/common/aws/interfaces/aws.ses-service.interface.ts b/src/modules/aws/interfaces/aws.ses-service.interface.ts similarity index 97% rename from src/common/aws/interfaces/aws.ses-service.interface.ts rename to src/modules/aws/interfaces/aws.ses-service.interface.ts index 447fde757..8fd2348dc 100644 --- a/src/common/aws/interfaces/aws.ses-service.interface.ts +++ b/src/modules/aws/interfaces/aws.ses-service.interface.ts @@ -13,7 +13,7 @@ import { AwsSESSendBulkDto, AwsSESSendDto, AwsSESUpdateTemplateDto, -} from 'src/common/aws/dtos/aws.ses.dto'; +} from 'src/modules/aws/dtos/aws.ses.dto'; export interface IAwsSESService { listTemplates(nextToken?: string): Promise; diff --git a/src/common/aws/repository/entities/aws.s3-multipart-part.entity.ts b/src/modules/aws/repository/entities/aws.s3-multipart-part.entity.ts similarity index 100% rename from src/common/aws/repository/entities/aws.s3-multipart-part.entity.ts rename to src/modules/aws/repository/entities/aws.s3-multipart-part.entity.ts diff --git a/src/common/aws/repository/entities/aws.s3-multipart.entity.ts b/src/modules/aws/repository/entities/aws.s3-multipart.entity.ts similarity index 91% rename from src/common/aws/repository/entities/aws.s3-multipart.entity.ts rename to src/modules/aws/repository/entities/aws.s3-multipart.entity.ts index fef12bf31..dc096c27f 100644 --- a/src/common/aws/repository/entities/aws.s3-multipart.entity.ts +++ b/src/modules/aws/repository/entities/aws.s3-multipart.entity.ts @@ -1,12 +1,12 @@ -import { - AwsS3MultipartPartEntity, - AwsS3MultipartPartSchema, -} from 'src/common/aws/repository/entities/aws.s3-multipart-part.entity'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; +import { + AwsS3MultipartPartEntity, + AwsS3MultipartPartSchema, +} from 'src/modules/aws/repository/entities/aws.s3-multipart-part.entity'; @DatabaseEntity({ timestamps: false, _id: false }) export class AwsS3MultipartEntity { diff --git a/src/common/aws/repository/entities/aws.s3.entity.ts b/src/modules/aws/repository/entities/aws.s3.entity.ts similarity index 100% rename from src/common/aws/repository/entities/aws.s3.entity.ts rename to src/modules/aws/repository/entities/aws.s3.entity.ts diff --git a/src/common/aws/services/aws.s3.service.ts b/src/modules/aws/services/aws.s3.service.ts similarity index 80% rename from src/common/aws/services/aws.s3.service.ts rename to src/modules/aws/services/aws.s3.service.ts index c7e282f33..8900153f8 100644 --- a/src/common/aws/services/aws.s3.service.ts +++ b/src/modules/aws/services/aws.s3.service.ts @@ -1,12 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { - IAwsS3PutItem, - IAwsS3PutItemOptions, - IAwsS3PutItemWithAclOptions, - IAwsS3RandomFilename, -} from 'src/common/aws/interfaces/aws.interface'; -import { IAwsS3Service } from 'src/common/aws/interfaces/aws.s3-service.interface'; import { Readable } from 'stream'; import { S3Client, @@ -52,24 +45,31 @@ import { ObjectCannedACL, CompletedPart, } from '@aws-sdk/client-s3'; -import { HelperStringService } from 'src/common/helper/services/helper.string.service'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; +import { IAwsS3Service } from 'src/modules/aws/interfaces/aws.s3-service.interface'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; +import { + IAwsS3PutItem, + IAwsS3PutItemOptions, + IAwsS3PutItemWithAclOptions, + IAwsS3PutPresignUrlFile, + IAwsS3PutPresignUrlOptions, +} from 'src/modules/aws/interfaces/aws.interface'; import { AwsS3MultipartDto, AwsS3MultipartPartDto, -} from 'src/common/aws/dtos/aws.s3-multipart.dto'; -import { AWS_S3_MAX_PART_NUMBER } from 'src/common/aws/constants/aws.constant'; +} from 'src/modules/aws/dtos/aws.s3-multipart.dto'; +import { AWS_S3_MAX_PART_NUMBER } from 'src/modules/aws/constants/aws.constant'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { AwsS3PresignUrlDto } from 'src/modules/aws/dtos/aws.s3-presign-url.dto'; @Injectable() export class AwsS3Service implements IAwsS3Service { private readonly s3Client: S3Client; private readonly bucket: string; private readonly baseUrl: string; + private readonly presignUrlExpired: number; - constructor( - private readonly configService: ConfigService, - private readonly helperStringService: HelperStringService - ) { + constructor(private readonly configService: ConfigService) { this.s3Client = new S3Client({ credentials: { accessKeyId: this.configService.get( @@ -84,6 +84,9 @@ export class AwsS3Service implements IAwsS3Service { this.bucket = this.configService.get('aws.s3.bucket'); this.baseUrl = this.configService.get('aws.s3.baseUrl'); + this.presignUrlExpired = this.configService.get( + 'aws.s3.presignUrlExpired' + ); } async checkBucketExistence(): Promise { @@ -119,10 +122,10 @@ export class AwsS3Service implements IAwsS3Service { } } - async listItemInBucket(prefix?: string): Promise { + async listItemInBucket(path?: string): Promise { const command: ListObjectsV2Command = new ListObjectsV2Command({ Bucket: this.bucket, - Prefix: prefix, + Prefix: path, }); try { @@ -148,7 +151,7 @@ export class AwsS3Service implements IAwsS3Service { path, pathWithFilename: val.Key, filename: filename, - completedUrl: `${this.baseUrl}/${val.Key}`, + completedUrl: `${this.baseUrl}${val.Key}`, baseUrl: this.baseUrl, mime, size: val.Size, @@ -184,18 +187,17 @@ export class AwsS3Service implements IAwsS3Service { file: IAwsS3PutItem, options?: IAwsS3PutItemOptions ): Promise { - let path: string = options?.path; - path = path?.startsWith('/') ? path.replace('/', '') : path; - + const path: string = `/${options?.path?.replace(/^\/*|\/*$/g, '') ?? ''}`; const mime: string = file.originalname.substring( file.originalname.lastIndexOf('.') + 1, file.originalname.length ); const filename = options?.customFilename - ? `${options?.customFilename}.${mime}` - : file.originalname; + ? `${options?.customFilename.replace(/^\/*|\/*$/g, '')}.${mime}` + : file.originalname.replace(/^\/*|\/*$/g, ''); const content: string | Uint8Array | Buffer = file.buffer; - const key: string = path ? `${path}/${filename}` : filename; + const key: string = + path === '/' ? `${path}${filename}` : `${path}/${filename}`; const command: PutObjectCommand = new PutObjectCommand({ Bucket: this.bucket, Key: key, @@ -213,7 +215,7 @@ export class AwsS3Service implements IAwsS3Service { path, pathWithFilename: key, filename: filename, - completedUrl: `${this.baseUrl}/${key}`, + completedUrl: `${this.baseUrl}${key}`, baseUrl: this.baseUrl, mime, size: file.size, @@ -227,8 +229,7 @@ export class AwsS3Service implements IAwsS3Service { file: IAwsS3PutItem, options?: IAwsS3PutItemWithAclOptions ): Promise { - let path: string = options?.path; - path = path?.startsWith('/') ? path.replace('/', '') : path; + const path: string = `/${options?.path?.replace(/^\/*|\/*$/g, '') ?? ''}`; const acl: ObjectCannedACL = options?.acl ? (options.acl as ObjectCannedACL) : ObjectCannedACL.public_read; @@ -238,10 +239,12 @@ export class AwsS3Service implements IAwsS3Service { file.originalname.length ); const filename = options?.customFilename - ? `${options?.customFilename}.${mime}` - : file.originalname; + ? `${options?.customFilename.replace(/^\/*|\/*$/g, '')}.${mime}` + : file.originalname.replace(/^\/*|\/*$/g, ''); const content: string | Uint8Array | Buffer = file.buffer; - const key: string = path ? `${path}/${filename}` : filename; + + const key: string = + path === '/' ? `${path}${filename}` : `${path}/${filename}`; const command: PutObjectCommand = new PutObjectCommand({ Bucket: this.bucket, Key: key, @@ -260,7 +263,7 @@ export class AwsS3Service implements IAwsS3Service { path, pathWithFilename: key, filename: filename, - completedUrl: `${this.baseUrl}/${key}`, + completedUrl: `${this.baseUrl}${key}`, baseUrl: this.baseUrl, mime, size: file.size, @@ -363,17 +366,18 @@ export class AwsS3Service implements IAwsS3Service { `Max part number is greater than ${AWS_S3_MAX_PART_NUMBER}` ); } - let path: string = options?.path ?? '/'; - path = path.startsWith('/') ? path.replace('/', '') : path; + const path: string = `/${options?.path?.replace(/^\/*|\/*$/g, '') ?? ''}`; const mime: string = file.originalname.substring( file.originalname.lastIndexOf('.') + 1, file.originalname.length ); const filename = options?.customFilename - ? `${options?.customFilename}.${mime}` - : file.originalname; - const key: string = path ? `${path}/${filename}` : filename; + ? `${options?.customFilename.replace(/^\/*|\/*$/g, '')}.${mime}` + : file.originalname.replace(/^\/*|\/*$/g, ''); + + const key: string = + path === '/' ? `${path}${filename}` : `${path}/${filename}`; const multiPartCommand: CreateMultipartUploadCommand = new CreateMultipartUploadCommand({ Bucket: this.bucket, @@ -392,7 +396,7 @@ export class AwsS3Service implements IAwsS3Service { path, pathWithFilename: key, filename: filename, - completedUrl: `${this.baseUrl}/${key}`, + completedUrl: `${this.baseUrl}${key}`, baseUrl: this.baseUrl, mime, size: 0, @@ -410,8 +414,7 @@ export class AwsS3Service implements IAwsS3Service { maxPartNumber: number, options?: IAwsS3PutItemWithAclOptions ): Promise { - let path: string = options?.path ?? '/'; - path = path.startsWith('/') ? path.replace('/', '') : path; + const path: string = `/${options?.path?.replace(/^\/*|\/*$/g, '') ?? ''}`; const acl: ObjectCannedACL = options?.acl ? (options.acl as ObjectCannedACL) : ObjectCannedACL.public_read; @@ -421,9 +424,11 @@ export class AwsS3Service implements IAwsS3Service { file.originalname.length ); const filename = options?.customFilename - ? `${options?.customFilename}.${mime}` - : file.originalname; - const key: string = path ? `${path}/${filename}` : filename; + ? `${options?.customFilename.replace(/^\/*|\/*$/g, '')}.${mime}` + : file.originalname.replace(/^\/*|\/*$/g, ''); + + const key: string = + path === '/' ? `${path}${filename}` : `${path}/${filename}`; const multiPartCommand: CreateMultipartUploadCommand = new CreateMultipartUploadCommand({ Bucket: this.bucket, @@ -443,7 +448,7 @@ export class AwsS3Service implements IAwsS3Service { path, pathWithFilename: key, filename: filename, - completedUrl: `${this.baseUrl}/${key}`, + completedUrl: `${this.baseUrl}${key}`, baseUrl: this.baseUrl, mime, size: 0, @@ -541,16 +546,42 @@ export class AwsS3Service implements IAwsS3Service { } } - async getFilenameFromCompletedUrl(completedUrl: string): Promise { - return completedUrl.replace(`${this.baseUrl}`, ''); - } + async setPresignUrl( + { filename, size, duration }: IAwsS3PutPresignUrlFile, + options?: IAwsS3PutPresignUrlOptions + ): Promise { + try { + const path: string = `/${options?.path?.replace(/^\/*|\/*$/g, '') ?? ''}`; + const key: string = + path === '/' ? `${path}${filename}` : `${path}/${filename}`; + const mime: string = filename.substring( + filename.lastIndexOf('.') + 1, + filename.length + ); - async createRandomFilename(path?: string): Promise { - const filename: string = this.helperStringService.random(20); + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: mime, + }); + const presignUrl = await getSignedUrl(this.s3Client, command, { + expiresIn: this.presignUrlExpired, + }); - return { - path: path ?? '/', - customFilename: filename, - }; + return { + bucket: this.bucket, + pathWithFilename: key, + path, + completedUrl: presignUrl, + expiredIn: this.presignUrlExpired, + size, + mime, + filename, + baseUrl: this.baseUrl, + duration, + }; + } catch (err) { + throw err; + } } } diff --git a/src/common/aws/services/aws.ses.service.ts b/src/modules/aws/services/aws.ses.service.ts similarity index 98% rename from src/common/aws/services/aws.ses.service.ts rename to src/modules/aws/services/aws.ses.service.ts index f5208b980..2dbfcaaee 100644 --- a/src/common/aws/services/aws.ses.service.ts +++ b/src/modules/aws/services/aws.ses.service.ts @@ -30,8 +30,8 @@ import { AwsSESSendBulkDto, AwsSESSendDto, AwsSESUpdateTemplateDto, -} from 'src/common/aws/dtos/aws.ses.dto'; -import { IAwsSESService } from 'src/common/aws/interfaces/aws.ses-service.interface'; +} from 'src/modules/aws/dtos/aws.ses.dto'; +import { IAwsSESService } from 'src/modules/aws/interfaces/aws.ses-service.interface'; @Injectable() export class AwsSESService implements IAwsSESService { diff --git a/src/modules/country/constants/country.status-code.constant.ts b/src/modules/country/constants/country.status-code.constant.ts deleted file mode 100644 index f1b4aea46..000000000 --- a/src/modules/country/constants/country.status-code.constant.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ENUM_COUNTRY_STATUS_CODE_ERROR { - NOT_FOUND_ERROR = 5140, - IS_ACTIVE_ERROR = 5141, - INACTIVE_ERROR = 5142, - EXIST_ERROR = 5143, -} diff --git a/src/modules/country/controllers/country.admin.controller.ts b/src/modules/country/controllers/country.admin.controller.ts index 8e10e0982..1580ce9ae 100644 --- a/src/modules/country/controllers/country.admin.controller.ts +++ b/src/modules/country/controllers/country.admin.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Get, Param, Patch } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; import { PaginationQuery, PaginationQueryFilterInBoolean, @@ -12,11 +12,11 @@ import { ENUM_POLICY_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; import { PolicyAbilityProtected, PolicyRoleProtected, -} from 'src/common/policy/decorators/policy.decorator'; +} from 'src/modules/policy/decorators/policy.decorator'; import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; import { Response, @@ -31,17 +31,11 @@ import { COUNTRY_DEFAULT_IS_ACTIVE, } from 'src/modules/country/constants/country.list.constant'; import { - CountryAdminActiveDoc, CountryAdminGetDoc, - CountryAdminInactiveDoc, CountryAdminListDoc, } from 'src/modules/country/docs/country.admin.doc'; import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; -import { - CountryActivePipe, - CountryInactivePipe, -} from 'src/modules/country/pipes/country.is-active.pipe'; import { CountryParsePipe } from 'src/modules/country/pipes/country.parse.pipe'; import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; import { CountryService } from 'src/modules/country/services/country.service'; @@ -65,7 +59,7 @@ export class CountryAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/list') async list( @PaginationQuery({ @@ -122,52 +116,4 @@ export class CountryAdminController { await this.countryService.mapGet(country); return { data: mapped }; } - - @CountryAdminInactiveDoc() - @Response('country.inactive') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.COUNTRY, - action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.UPDATE], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Patch('/update/:country/inactive') - async inactive( - @Param( - 'country', - RequestRequiredPipe, - CountryParsePipe, - CountryActivePipe - ) - country: CountryDoc - ): Promise { - await this.countryService.inactive(country); - - return; - } - - @CountryAdminActiveDoc() - @Response('country.active') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.COUNTRY, - action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.UPDATE], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Patch('/update/:country/active') - async active( - @Param( - 'country', - RequestRequiredPipe, - CountryParsePipe, - CountryInactivePipe - ) - country: CountryDoc - ): Promise { - await this.countryService.active(country); - - return; - } } diff --git a/src/modules/country/controllers/country.private.controller.ts b/src/modules/country/controllers/country.system.controller.ts similarity index 88% rename from src/modules/country/controllers/country.private.controller.ts rename to src/modules/country/controllers/country.system.controller.ts index 1703db201..8bbfec5cb 100644 --- a/src/modules/country/controllers/country.private.controller.ts +++ b/src/modules/country/controllers/country.system.controller.ts @@ -1,31 +1,31 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ApiKeyPrivateProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { ApiKeySystemProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { PaginationQuery } from 'src/common/pagination/decorators/pagination.decorator'; import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; import { ResponsePaging } from 'src/common/response/decorators/response.decorator'; import { IResponsePaging } from 'src/common/response/interfaces/response.interface'; import { COUNTRY_DEFAULT_AVAILABLE_SEARCH } from 'src/modules/country/constants/country.list.constant'; -import { CountryPrivateListDoc } from 'src/modules/country/docs/country.private.doc'; import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; import { CountryService } from 'src/modules/country/services/country.service'; +import { CountrySystemListDoc } from 'src/modules/country/docs/country.system.doc'; -@ApiTags('modules.private.country') +@ApiTags('modules.system.country') @Controller({ version: '1', path: '/country', }) -export class CountryPrivateController { +export class CountrySystemController { constructor( private readonly countryService: CountryService, private readonly paginationService: PaginationService ) {} - @CountryPrivateListDoc() + @CountrySystemListDoc() @ResponsePaging('country.list') - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/list') async list( @PaginationQuery({ diff --git a/src/modules/country/docs/country.admin.doc.ts b/src/modules/country/docs/country.admin.doc.ts index 563b8b54a..38963ed7a 100644 --- a/src/modules/country/docs/country.admin.doc.ts +++ b/src/modules/country/docs/country.admin.doc.ts @@ -45,37 +45,3 @@ export function CountryAdminGetDoc(): MethodDecorator { }) ); } - -export function CountryAdminActiveDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'make country be active', - }), - DocRequest({ - params: CountryDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponse('country.active') - ); -} - -export function CountryAdminInactiveDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'make country be inactive', - }), - DocRequest({ - params: CountryDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponse('country.inactive') - ); -} diff --git a/src/modules/country/docs/country.private.doc.ts b/src/modules/country/docs/country.system.doc.ts similarity index 90% rename from src/modules/country/docs/country.private.doc.ts rename to src/modules/country/docs/country.system.doc.ts index 04f57d482..380c7510f 100644 --- a/src/modules/country/docs/country.private.doc.ts +++ b/src/modules/country/docs/country.system.doc.ts @@ -6,7 +6,7 @@ import { import { Doc } from 'src/common/doc/decorators/doc.decorator'; import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; -export function CountryPrivateListDoc(): MethodDecorator { +export function CountrySystemListDoc(): MethodDecorator { return applyDecorators( Doc({ summary: 'get all list country' }), DocAuth({ xApiKey: true }), diff --git a/src/modules/country/dtos/request/country.create.request.dto.ts b/src/modules/country/dtos/request/country.create.request.dto.ts index c77cf2452..03ccc2fc4 100644 --- a/src/modules/country/dtos/request/country.create.request.dto.ts +++ b/src/modules/country/dtos/request/country.create.request.dto.ts @@ -24,7 +24,7 @@ export class CountryCreateRequestDto { @IsString() @MaxLength(100) @MinLength(1) - readonly name: string; + name: string; @ApiProperty({ required: true, @@ -39,7 +39,7 @@ export class CountryCreateRequestDto { @MaxLength(2) @MinLength(2) @Transform(({ value }) => value.toUpperCase()) - readonly alpha2Code: string; + alpha2Code: string; @ApiProperty({ required: true, @@ -54,7 +54,7 @@ export class CountryCreateRequestDto { @MaxLength(3) @MinLength(3) @Transform(({ value }) => value.toUpperCase()) - readonly alpha3Code: string; + alpha3Code: string; @ApiProperty({ required: true, @@ -68,7 +68,7 @@ export class CountryCreateRequestDto { @IsString() @MaxLength(3) @MinLength(1) - readonly numericCode: string; + numericCode: string; @ApiProperty({ required: true, @@ -82,7 +82,7 @@ export class CountryCreateRequestDto { @IsString() @MaxLength(2) @MinLength(2) - readonly fipsCode: string; + fipsCode: string; @ApiProperty({ required: true, @@ -98,7 +98,7 @@ export class CountryCreateRequestDto { @IsNotEmpty({ each: true }) @IsString({ each: true }) @MaxLength(4, { each: true }) - readonly phoneCode: string[]; + phoneCode: string[]; @ApiProperty({ required: true, @@ -106,7 +106,7 @@ export class CountryCreateRequestDto { }) @IsNotEmpty() @IsString() - readonly continent: string; + continent: string; @ApiProperty({ required: true, @@ -114,7 +114,7 @@ export class CountryCreateRequestDto { }) @IsNotEmpty() @IsString() - readonly timeZone: string; + timeZone: string; @ApiProperty({ required: false, @@ -123,5 +123,5 @@ export class CountryCreateRequestDto { }) @IsOptional() @IsString() - readonly domain?: string; + domain?: string; } diff --git a/src/modules/country/dtos/response/country.get.response.dto.ts b/src/modules/country/dtos/response/country.get.response.dto.ts index 6d25220b2..4f52056bd 100644 --- a/src/modules/country/dtos/response/country.get.response.dto.ts +++ b/src/modules/country/dtos/response/country.get.response.dto.ts @@ -1,10 +1,10 @@ import { faker } from '@faker-js/faker'; import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude } from 'class-transformer'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; +import { Exclude, Type } from 'class-transformer'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; -export class CountryGetResponseDto extends DatabaseIdResponseDto { +export class CountryGetResponseDto extends DatabaseDto { @ApiProperty({ required: true, type: String, @@ -13,7 +13,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { maxLength: 100, minLength: 1, }) - readonly name: string; + name: string; @ApiProperty({ required: true, @@ -23,7 +23,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { maxLength: 2, minLength: 2, }) - readonly alpha2Code: string; + alpha2Code: string; @ApiProperty({ required: true, @@ -33,7 +33,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { maxLength: 3, minLength: 3, }) - readonly alpha3Code: string; + alpha3Code: string; @ApiProperty({ required: true, @@ -43,7 +43,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { maxLength: 3, minLength: 3, }) - readonly numericCode: string; + numericCode: string; @ApiProperty({ required: true, @@ -53,7 +53,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { maxLength: 2, minLength: 2, }) - readonly fipsCode: string; + fipsCode: string; @ApiProperty({ required: true, @@ -65,32 +65,33 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { isArray: true, default: [], }) - readonly phoneCode: string[]; + phoneCode: string[]; @ApiProperty({ required: true, example: faker.location.country(), }) - readonly continent: string; + continent: string; @ApiProperty({ required: true, example: faker.location.timeZone(), }) - readonly timeZone: string; + timeZone: string; @ApiProperty({ required: false, description: 'Top level domain', example: faker.internet.domainSuffix(), }) - readonly domain?: string; + domain?: string; @ApiProperty({ required: false, - type: () => AwsS3Dto, + type: AwsS3Dto, }) - readonly image?: AwsS3Dto; + @Type(() => AwsS3Dto) + image?: AwsS3Dto; @ApiProperty({ description: 'Date created at', @@ -98,7 +99,7 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { required: true, nullable: false, }) - readonly createdAt: Date; + createdAt: Date; @ApiProperty({ description: 'Date updated at', @@ -106,9 +107,9 @@ export class CountryGetResponseDto extends DatabaseIdResponseDto { required: true, nullable: false, }) - readonly updatedAt: Date; + updatedAt: Date; @ApiHideProperty() @Exclude() - readonly deletedAt?: Date; + deletedAt?: Date; } diff --git a/src/modules/country/dtos/response/country.list.response.dto.ts b/src/modules/country/dtos/response/country.list.response.dto.ts index 7a20dc692..105017e61 100644 --- a/src/modules/country/dtos/response/country.list.response.dto.ts +++ b/src/modules/country/dtos/response/country.list.response.dto.ts @@ -12,25 +12,25 @@ export class CountryListResponseDto extends OmitType(CountryGetResponseDto, [ ] as const) { @ApiHideProperty() @Exclude() - readonly alpha3Code: string; + alpha3Code: string; @ApiHideProperty() @Exclude() - readonly fipsCode: string; + fipsCode: string; @ApiHideProperty() @Exclude() - readonly continent: string; + continent: string; @ApiHideProperty() @Exclude() - readonly domain?: string; + domain?: string; @ApiHideProperty() @Exclude() - readonly timeZone: string; + timeZone: string; @ApiHideProperty() @Exclude() - readonly numericCode: string; + numericCode: string; } diff --git a/src/modules/country/dtos/response/country.short.response.dto.ts b/src/modules/country/dtos/response/country.short.response.dto.ts new file mode 100644 index 000000000..ec47ddd03 --- /dev/null +++ b/src/modules/country/dtos/response/country.short.response.dto.ts @@ -0,0 +1,16 @@ +import { ApiHideProperty, OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; + +export class CountryShortResponseDto extends OmitType(CountryListResponseDto, [ + 'createdAt', + 'updatedAt', +]) { + @ApiHideProperty() + @Exclude() + createdAt: Date; + + @ApiHideProperty() + @Exclude() + updatedAt: Date; +} diff --git a/src/modules/country/enums/country.status-code.enum.ts b/src/modules/country/enums/country.status-code.enum.ts new file mode 100644 index 000000000..7e8ee9e94 --- /dev/null +++ b/src/modules/country/enums/country.status-code.enum.ts @@ -0,0 +1,6 @@ +export enum ENUM_COUNTRY_STATUS_CODE_ERROR { + NOT_FOUND = 5140, + IS_ACTIVE = 5141, + INACTIVE = 5142, + EXIST = 5143, +} diff --git a/src/modules/country/interfaces/country.service.interface.ts b/src/modules/country/interfaces/country.service.interface.ts index 829baddb5..847a9e15f 100644 --- a/src/modules/country/interfaces/country.service.interface.ts +++ b/src/modules/country/interfaces/country.service.interface.ts @@ -1,15 +1,18 @@ import { IDatabaseCreateManyOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, - IDatabaseSaveOptions, + IDatabaseOptions, } from 'src/common/database/interfaces/database.interface'; import { CountryCreateRequestDto } from 'src/modules/country/dtos/request/country.create.request.dto'; import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; -import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; +import { + CountryDoc, + CountryEntity, +} from 'src/modules/country/repository/entities/country.entity'; export interface ICountryService { findAll( @@ -18,52 +21,42 @@ export interface ICountryService { ): Promise; findOne( find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByAlpha2( - alpha2: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; findOneByName( name: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions + ): Promise; + findOneByAlpha2( + alpha2: string, + options?: IDatabaseOptions ): Promise; findOneActiveByPhoneCode( phoneCode: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; + findOneById(_id: string, options?: IDatabaseOptions): Promise; findOneActiveById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; getTotal( find?: Record, options?: IDatabaseGetTotalOptions ): Promise; - active( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise; - inactive( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise; - delete( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise; deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise; createMany( data: CountryCreateRequestDto[], options?: IDatabaseCreateManyOptions ): Promise; - mapList(data: CountryDoc[]): Promise; - mapGet(county: CountryDoc): Promise; + mapList( + countries: CountryDoc[] | CountryEntity[] + ): Promise; + mapGet(country: CountryDoc | CountryEntity): Promise; + mapShort( + countries: CountryDoc[] | CountryEntity[] + ): Promise; } diff --git a/src/modules/country/pipes/country.is-active.pipe.ts b/src/modules/country/pipes/country.is-active.pipe.ts deleted file mode 100644 index 6aa81ac3d..000000000 --- a/src/modules/country/pipes/country.is-active.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; -import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/constants/country.status-code.constant'; -import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; - -@Injectable() -export class CountryActivePipe implements PipeTransform { - async transform(value: CountryDoc): Promise { - if (!value.isActive) { - throw new BadRequestException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, - message: 'country.error.isActiveInvalid', - }); - } - - return value; - } -} - -@Injectable() -export class CountryInactivePipe implements PipeTransform { - async transform(value: CountryDoc): Promise { - if (value.isActive) { - throw new BadRequestException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, - message: 'country.error.isActiveInvalid', - }); - } - - return value; - } -} diff --git a/src/modules/country/pipes/country.parse.pipe.ts b/src/modules/country/pipes/country.parse.pipe.ts index f85a46e69..55447975a 100644 --- a/src/modules/country/pipes/country.parse.pipe.ts +++ b/src/modules/country/pipes/country.parse.pipe.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; -import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/constants/country.status-code.constant'; +import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/enums/country.status-code.enum'; import { CountryDoc } from 'src/modules/country/repository/entities/country.entity'; import { CountryService } from 'src/modules/country/services/country.service'; @@ -12,7 +12,7 @@ export class CountryParsePipe implements PipeTransform { await this.countryService.findOneById(value); if (!country) { throw new NotFoundException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND, message: 'country.error.notFound', }); } diff --git a/src/modules/country/repository/entities/country.entity.ts b/src/modules/country/repository/entities/country.entity.ts index 4db0a9d46..1cafd6c4d 100644 --- a/src/modules/country/repository/entities/country.entity.ts +++ b/src/modules/country/repository/entities/country.entity.ts @@ -1,17 +1,17 @@ -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; -import { AwsS3Schema } from 'src/common/aws/repository/entities/aws.s3.entity'; -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; +import { AwsS3Schema } from 'src/modules/aws/repository/entities/aws.s3.entity'; export const CountryTableName = 'Countries'; @DatabaseEntity({ collection: CountryTableName }) -export class CountryEntity extends DatabaseMongoUUIDEntityAbstract { +export class CountryEntity extends DatabaseEntityAbstract { @DatabaseProp({ required: true, index: true, @@ -89,13 +89,6 @@ export class CountryEntity extends DatabaseMongoUUIDEntityAbstract { schema: AwsS3Schema, }) image?: AwsS3Dto; - - @DatabaseProp({ - required: true, - index: true, - default: true, - }) - isActive: boolean; } export const CountrySchema = DatabaseSchema(CountryEntity); diff --git a/src/modules/country/repository/repositories/country.repository.ts b/src/modules/country/repository/repositories/country.repository.ts index 4c3415537..dcda00c49 100644 --- a/src/modules/country/repository/repositories/country.repository.ts +++ b/src/modules/country/repository/repositories/country.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/database.repository.abstract'; import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; import { CountryDoc, @@ -8,7 +8,7 @@ import { } from 'src/modules/country/repository/entities/country.entity'; @Injectable() -export class CountryRepository extends DatabaseMongoUUIDRepositoryAbstract< +export class CountryRepository extends DatabaseRepositoryAbstract< CountryEntity, CountryDoc > { diff --git a/src/modules/country/services/country.service.ts b/src/modules/country/services/country.service.ts index f5d980844..ec61c698a 100644 --- a/src/modules/country/services/country.service.ts +++ b/src/modules/country/services/country.service.ts @@ -1,17 +1,18 @@ import { Injectable } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; +import { Document } from 'mongoose'; import { DatabaseQueryContain } from 'src/common/database/decorators/database.decorator'; import { IDatabaseCreateManyOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, - IDatabaseSaveOptions, + IDatabaseOptions, } from 'src/common/database/interfaces/database.interface'; import { CountryCreateRequestDto } from 'src/modules/country/dtos/request/country.create.request.dto'; import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; import { ICountryService } from 'src/modules/country/interfaces/country.service.interface'; import { CountryDoc, @@ -27,21 +28,21 @@ export class CountryService implements ICountryService { find?: Record, options?: IDatabaseFindAllOptions ): Promise { - return this.countryRepository.findAll(find, options); + return this.countryRepository.findAll(find, options); } async findOne( find: Record, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOne(find, options); + return this.countryRepository.findOne(find, options); } async findOneByName( name: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOne( + return this.countryRepository.findOne( DatabaseQueryContain('name', name), options ); @@ -49,9 +50,9 @@ export class CountryService implements ICountryService { async findOneByAlpha2( alpha2: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOne( + return this.countryRepository.findOne( DatabaseQueryContain('alpha2Code', alpha2), options ); @@ -59,9 +60,9 @@ export class CountryService implements ICountryService { async findOneActiveByPhoneCode( phoneCode: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOne( + return this.countryRepository.findOne( { phoneCode, isActive: true, @@ -72,19 +73,16 @@ export class CountryService implements ICountryService { async findOneById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOneById(_id, options); + return this.countryRepository.findOneById(_id, options); } async findOneActiveById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.countryRepository.findOne( - { _id, isActive: true }, - options - ); + return this.countryRepository.findOne({ _id, isActive: true }, options); } async getTotal( @@ -94,44 +92,25 @@ export class CountryService implements ICountryService { return this.countryRepository.getTotal(find, options); } - async active( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise { - repository.isActive = true; - - return this.countryRepository.save(repository, options); - } - - async inactive( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise { - repository.isActive = false; - - return this.countryRepository.save(repository, options); - } - - async delete( - repository: CountryDoc, - options?: IDatabaseSaveOptions - ): Promise { - return this.countryRepository.softDelete(repository, options); - } - async deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise { - return this.countryRepository.deleteMany(find, options); + try { + await this.countryRepository.deleteMany(find, options); + + return true; + } catch (error: unknown) { + throw error; + } } async createMany( data: CountryCreateRequestDto[], options?: IDatabaseCreateManyOptions ): Promise { - return this.countryRepository.createMany( - data.map( + try { + const entities: CountryEntity[] = data.map( ({ name, alpha2Code, @@ -153,20 +132,47 @@ export class CountryService implements ICountryService { create.phoneCode = phoneCode; create.timeZone = timeZone; create.domain = domain; - create.isActive = true; return create; } - ), - options + ) as CountryEntity[]; + + await this.countryRepository.createMany(entities, options); + + return true; + } catch (error: unknown) { + throw error; + } + } + + async mapList( + countries: CountryDoc[] | CountryEntity[] + ): Promise { + return plainToInstance( + CountryListResponseDto, + countries.map((e: CountryDoc | CountryEntity) => + e instanceof Document ? e.toObject() : e + ) ); } - async mapList(countries: CountryDoc[]): Promise { - return plainToInstance(CountryListResponseDto, countries); + async mapGet( + country: CountryDoc | CountryEntity + ): Promise { + return plainToInstance( + CountryGetResponseDto, + country instanceof Document ? country.toObject() : country + ); } - async mapGet(county: CountryDoc): Promise { - return plainToInstance(CountryGetResponseDto, county); + async mapShort( + countries: CountryDoc[] | CountryEntity[] + ): Promise { + return plainToInstance( + CountryShortResponseDto, + countries.map((e: CountryDoc | CountryEntity) => + e instanceof Document ? e.toObject() : e + ) + ); } } diff --git a/src/modules/device/device.module.ts b/src/modules/device/device.module.ts new file mode 100644 index 000000000..9ee5f2fa4 --- /dev/null +++ b/src/modules/device/device.module.ts @@ -0,0 +1 @@ +// TODO: DEVICE MODULE diff --git a/src/modules/email/dtos/email.send.dto.ts b/src/modules/email/dtos/email.send.dto.ts index 96831a6ec..a826633f7 100644 --- a/src/modules/email/dtos/email.send.dto.ts +++ b/src/modules/email/dtos/email.send.dto.ts @@ -6,11 +6,11 @@ export class EmailSendDto { type: 'string', example: faker.person.fullName(), }) - readonly name: string; + name: string; @ApiProperty({ type: 'string', example: faker.internet.email(), }) - readonly email: string; + email: string; } diff --git a/src/modules/email/dtos/email.send-temp-password.dto.ts b/src/modules/email/dtos/email.temp-password.dto.ts similarity index 86% rename from src/modules/email/dtos/email.send-temp-password.dto.ts rename to src/modules/email/dtos/email.temp-password.dto.ts index b6b2a4796..eeba66551 100644 --- a/src/modules/email/dtos/email.send-temp-password.dto.ts +++ b/src/modules/email/dtos/email.temp-password.dto.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { ApiProperty } from '@nestjs/swagger'; -export class EmailSendTempPasswordDto { +export class EmailTempPasswordDto { @ApiProperty({ required: true, example: faker.string.alphanumeric(10), @@ -15,5 +15,5 @@ export class EmailSendTempPasswordDto { type: 'date', description: 'Expired at by date', }) - expiredAt: Date; + passwordExpiredAt: Date; } diff --git a/src/modules/email/dtos/email.welcome-admin.dto.ts b/src/modules/email/dtos/email.welcome-admin.dto.ts new file mode 100644 index 000000000..1ec1e2d8b --- /dev/null +++ b/src/modules/email/dtos/email.welcome-admin.dto.ts @@ -0,0 +1,3 @@ +import { EmailTempPasswordDto } from 'src/modules/email/dtos/email.temp-password.dto'; + +export class EmailWelcomeAdminDto extends EmailTempPasswordDto {} diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index 5a6a318ad..92a713444 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; +import { AwsModule } from 'src/modules/aws/aws.module'; import { EmailService } from 'src/modules/email/services/email.service'; -import { AwsModule } from 'src/common/aws/aws.module'; @Module({ imports: [AwsModule], diff --git a/src/modules/email/constants/email.enum.constant.ts b/src/modules/email/enums/email.enum.ts similarity index 62% rename from src/modules/email/constants/email.enum.constant.ts rename to src/modules/email/enums/email.enum.ts index 76e8ecba2..db2d0b1ac 100644 --- a/src/modules/email/constants/email.enum.constant.ts +++ b/src/modules/email/enums/email.enum.ts @@ -1,5 +1,6 @@ export enum ENUM_EMAIL { CHANGE_PASSWORD = 'CHANGE_PASSWORD', TEMP_PASSWORD = 'TEMP_PASSWORD', - WElCOME = 'WElCOME', + WELCOME = 'WELCOME', + WELCOME_ADMIN = 'WELCOME_ADMIN', } diff --git a/src/modules/email/interfaces/email.processor.interface.ts b/src/modules/email/interfaces/email.processor.interface.ts new file mode 100644 index 000000000..cbcb07137 --- /dev/null +++ b/src/modules/email/interfaces/email.processor.interface.ts @@ -0,0 +1,16 @@ +import { EmailSendDto } from 'src/modules/email/dtos/email.send.dto'; +import { EmailTempPasswordDto } from 'src/modules/email/dtos/email.temp-password.dto'; +import { EmailWelcomeAdminDto } from 'src/modules/email/dtos/email.welcome-admin.dto'; + +export interface IEmailProcessor { + processWelcome(data: EmailSendDto): Promise; + processWelcomeAdmin( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailWelcomeAdminDto + ): Promise; + processChangePassword(data: EmailSendDto): Promise; + processTempPassword( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailTempPasswordDto + ): Promise; +} diff --git a/src/modules/email/interfaces/email.service.interface.ts b/src/modules/email/interfaces/email.service.interface.ts index d40e47616..1017e7d5a 100644 --- a/src/modules/email/interfaces/email.service.interface.ts +++ b/src/modules/email/interfaces/email.service.interface.ts @@ -1,5 +1,7 @@ import { GetTemplateCommandOutput } from '@aws-sdk/client-ses'; import { EmailSendDto } from 'src/modules/email/dtos/email.send.dto'; +import { EmailTempPasswordDto } from 'src/modules/email/dtos/email.temp-password.dto'; +import { EmailWelcomeAdminDto } from 'src/modules/email/dtos/email.welcome-admin.dto'; export interface IEmailService { createChangePassword(): Promise; @@ -10,4 +12,18 @@ export interface IEmailService { getWelcome(): Promise; deleteWelcome(): Promise; sendWelcome({ name, email }: EmailSendDto): Promise; + createWelcomeAdmin(): Promise; + getWelcomeAdmin(): Promise; + deleteWelcomeAdmin(): Promise; + sendWelcomeAdmin( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailWelcomeAdminDto + ): Promise; + createTempPassword(): Promise; + getTempPassword(): Promise; + deleteTempPassword(): Promise; + sendTempPassword( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailTempPasswordDto + ): Promise; } diff --git a/src/modules/email/processors/email.processor.ts b/src/modules/email/processors/email.processor.ts new file mode 100644 index 000000000..6f4afcb73 --- /dev/null +++ b/src/modules/email/processors/email.processor.ts @@ -0,0 +1,95 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Job } from 'bullmq'; +import { EmailSendDto } from 'src/modules/email/dtos/email.send.dto'; +import { EmailTempPasswordDto } from 'src/modules/email/dtos/email.temp-password.dto'; +import { EmailWelcomeAdminDto } from 'src/modules/email/dtos/email.welcome-admin.dto'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; +import { IEmailProcessor } from 'src/modules/email/interfaces/email.processor.interface'; +import { EmailService } from 'src/modules/email/services/email.service'; +import { WorkerEmailDto } from 'src/worker/dtos/worker.email.dto'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; + +@Processor(ENUM_WORKER_QUEUES.EMAIL_QUEUE) +export class EmailProcessor extends WorkerHost implements IEmailProcessor { + private readonly debug: boolean; + private readonly logger = new Logger(EmailProcessor.name); + + constructor( + private readonly emailService: EmailService, + private readonly configService: ConfigService + ) { + super(); + + this.debug = this.configService.get('app.debug'); + } + + async process(job: Job): Promise { + try { + const jobName = job.name; + switch (jobName) { + case ENUM_EMAIL.TEMP_PASSWORD: + await this.processTempPassword( + job.data.send, + job.data.data as EmailTempPasswordDto + ); + + break; + case ENUM_EMAIL.CHANGE_PASSWORD: + await this.processChangePassword(job.data.send); + + break; + case ENUM_EMAIL.WELCOME_ADMIN: + await this.processWelcomeAdmin( + job.data.send, + job.data.data as EmailWelcomeAdminDto + ); + + break; + case ENUM_EMAIL.WELCOME: + default: + await this.processWelcome(job.data.send); + + break; + } + } catch (error: any) { + if (this.debug) { + this.logger.error(error); + } + } + + return; + } + + async processWelcome(data: EmailSendDto): Promise { + return this.emailService.sendWelcome(data); + } + + async processWelcomeAdmin( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailWelcomeAdminDto + ): Promise { + return this.emailService.sendWelcomeAdmin( + { + email, + name, + }, + { password, passwordExpiredAt } + ); + } + + async processChangePassword(data: EmailSendDto): Promise { + return this.emailService.sendChangePassword(data); + } + + async processTempPassword( + { name, email }: EmailSendDto, + { password, passwordExpiredAt }: EmailTempPasswordDto + ): Promise { + return this.emailService.sendTempPassword( + { email, name }, + { password, passwordExpiredAt } + ); + } +} diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 24d197d05..a77231742 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -1,26 +1,43 @@ -import { Injectable } from '@nestjs/common'; -import { AwsSESService } from 'src/common/aws/services/aws.ses.service'; -import { ENUM_EMAIL } from 'src/modules/email/constants/email.enum.constant'; +import { Injectable, Logger } from '@nestjs/common'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; import { title } from 'case'; import { ConfigService } from '@nestjs/config'; import { IEmailService } from 'src/modules/email/interfaces/email.service.interface'; import { readFileSync } from 'fs'; import { GetTemplateCommandOutput } from '@aws-sdk/client-ses'; import { EmailSendDto } from 'src/modules/email/dtos/email.send.dto'; -import { EmailSendTempPasswordDto } from 'src/modules/email/dtos/email.send-temp-password.dto'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { ENUM_HELPER_DATE_FORMAT } from 'src/common/helper/constants/helper.enum.constant'; +import { EmailTempPasswordDto } from 'src/modules/email/dtos/email.temp-password.dto'; +import { EmailWelcomeAdminDto } from 'src/modules/email/dtos/email.welcome-admin.dto'; +import { ENUM_HELPER_DATE_FORMAT } from 'src/common/helper/enums/helper.enum'; +import { AwsSESService } from 'src/modules/aws/services/aws.ses.service'; @Injectable() export class EmailService implements IEmailService { + private readonly debug: boolean; + private readonly logger = new Logger(EmailService.name); + private readonly fromEmail: string; + private readonly supportEmail: string; + + private readonly appName: string; + + private readonly clientUrl: string; constructor( private readonly awsSESService: AwsSESService, private readonly helperDateService: HelperDateService, private readonly configService: ConfigService ) { + this.debug = this.configService.get('app.debug'); + this.fromEmail = this.configService.get('email.fromEmail'); + this.supportEmail = + this.configService.get('email.supportEmail'); + + this.appName = this.configService.get('app.name'); + + this.clientUrl = this.configService.get('email.clientUrl'); } async createChangePassword(): Promise { @@ -36,20 +53,18 @@ export class EmailService implements IEmailService { return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } async getChangePassword(): Promise { - try { - const template = await this.awsSESService.getTemplate({ - name: ENUM_EMAIL.CHANGE_PASSWORD, - }); - - return template; - } catch (err: unknown) { - return; - } + return this.awsSESService.getTemplate({ + name: ENUM_EMAIL.CHANGE_PASSWORD, + }); } async deleteChangePassword(): Promise { @@ -60,6 +75,10 @@ export class EmailService implements IEmailService { return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } @@ -71,12 +90,19 @@ export class EmailService implements IEmailService { recipients: [email], sender: this.fromEmail, templateData: { + appName: this.appName, name: title(name), + supportEmail: this.supportEmail, + clientUrl: this.clientUrl, }, }); return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } @@ -84,57 +110,145 @@ export class EmailService implements IEmailService { async createWelcome(): Promise { try { await this.awsSESService.createTemplate({ - name: ENUM_EMAIL.WElCOME, + name: ENUM_EMAIL.WELCOME, subject: `Welcome`, htmlBody: readFileSync( - './templates/email.sign-up.template.html', + './templates/email.welcome.template.html', 'utf8' ), }); return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } async getWelcome(): Promise { + return this.awsSESService.getTemplate({ + name: ENUM_EMAIL.WELCOME, + }); + } + + async deleteWelcome(): Promise { try { - const template = await this.awsSESService.getTemplate({ - name: ENUM_EMAIL.WElCOME, + await this.awsSESService.deleteTemplate({ + name: ENUM_EMAIL.WELCOME, }); - return template; + return true; } catch (err: unknown) { - return; + if (this.debug) { + this.logger.error(err); + } + + return false; } } - async deleteWelcome(): Promise { + async sendWelcome({ name, email }: EmailSendDto): Promise { + try { + await this.awsSESService.send({ + templateName: ENUM_EMAIL.WELCOME, + recipients: [email], + sender: this.fromEmail, + templateData: { + appName: this.appName, + name: title(name), + email: title(email), + supportEmail: this.supportEmail, + clientUrl: this.clientUrl, + }, + }); + + return true; + } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + + return false; + } + } + + async createWelcomeAdmin(): Promise { + try { + await this.awsSESService.createTemplate({ + name: ENUM_EMAIL.WELCOME_ADMIN, + subject: `Welcome`, + htmlBody: readFileSync( + './templates/email.welcome-admin.template.html', + 'utf8' + ), + }); + + return true; + } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + + return false; + } + } + + async getWelcomeAdmin(): Promise { + return this.awsSESService.getTemplate({ + name: ENUM_EMAIL.WELCOME_ADMIN, + }); + } + + async deleteWelcomeAdmin(): Promise { try { await this.awsSESService.deleteTemplate({ - name: ENUM_EMAIL.WElCOME, + name: ENUM_EMAIL.WELCOME_ADMIN, }); return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } - async sendWelcome({ name, email }: EmailSendDto): Promise { + async sendWelcomeAdmin( + { name, email }: EmailSendDto, + { password: passwordString, passwordExpiredAt }: EmailWelcomeAdminDto + ): Promise { try { await this.awsSESService.send({ - templateName: ENUM_EMAIL.WElCOME, + templateName: ENUM_EMAIL.WELCOME, recipients: [email], sender: this.fromEmail, templateData: { + appName: this.appName, name: title(name), + email: title(email), + password: passwordString, + supportEmail: this.supportEmail, + clientUrl: this.clientUrl, + passwordExpiredAt: this.helperDateService.format( + passwordExpiredAt, + { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE_TIME, + } + ), }, }); return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } @@ -152,6 +266,10 @@ export class EmailService implements IEmailService { return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } @@ -164,6 +282,10 @@ export class EmailService implements IEmailService { return template; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return; } } @@ -176,13 +298,17 @@ export class EmailService implements IEmailService { return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } async sendTempPassword( { name, email }: EmailSendDto, - { password, expiredAt }: EmailSendTempPasswordDto + { password: passwordString, passwordExpiredAt }: EmailTempPasswordDto ): Promise { try { await this.awsSESService.send({ @@ -190,16 +316,26 @@ export class EmailService implements IEmailService { recipients: [email], sender: this.fromEmail, templateData: { + appName: this.appName, name: title(name), - password, - expiredAt: this.helperDateService.format(expiredAt, { - format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE_TIME, - }), + password: passwordString, + supportEmail: this.supportEmail, + clientUrl: this.clientUrl, + passwordExpiredAt: this.helperDateService.format( + passwordExpiredAt, + { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE_TIME, + } + ), }, }); return true; } catch (err: unknown) { + if (this.debug) { + this.logger.error(err); + } + return false; } } diff --git a/src/modules/email/templates/email.change-password.template.html b/src/modules/email/templates/email.change-password.template.html index 2831d4240..5fcb9a2bd 100644 --- a/src/modules/email/templates/email.change-password.template.html +++ b/src/modules/email/templates/email.change-password.template.html @@ -1 +1 @@ -Hi{name},
Change password success \ No newline at end of file +

Hi{name},


Change password successfully

Support Email:{supportEmail}

Visit us:{clientUrl}


By:{appName}

\ No newline at end of file diff --git a/src/modules/email/templates/email.sign-up.template.html b/src/modules/email/templates/email.sign-up.template.html deleted file mode 100644 index c281ee1a4..000000000 --- a/src/modules/email/templates/email.sign-up.template.html +++ /dev/null @@ -1 +0,0 @@ -Welcome{name},
Sign up success \ No newline at end of file diff --git a/src/modules/email/templates/email.temp-password.template.html b/src/modules/email/templates/email.temp-password.template.html index ff53cf571..4770defbb 100644 --- a/src/modules/email/templates/email.temp-password.template.html +++ b/src/modules/email/templates/email.temp-password.template.html @@ -1 +1 @@ -Hi{name},
Your password is {password}
Expired At {expiredAt} \ No newline at end of file +

Hi{name},


Your password is {password}

Expired At{passwordExpiredAt}


Support Email:{supportEmail}

Visit us:{clientUrl}


By:{appName}

\ No newline at end of file diff --git a/src/modules/email/templates/email.welcome-admin.template.html b/src/modules/email/templates/email.welcome-admin.template.html new file mode 100644 index 000000000..89113a7e1 --- /dev/null +++ b/src/modules/email/templates/email.welcome-admin.template.html @@ -0,0 +1 @@ +

Welcome{name},


Create by admin is successfully with email{email} and password{password}

Support Email:{supportEmail}

Visit us:{clientUrl}


By:{appName}

\ No newline at end of file diff --git a/src/modules/email/templates/email.welcome.template.html b/src/modules/email/templates/email.welcome.template.html new file mode 100644 index 000000000..df4f2437a --- /dev/null +++ b/src/modules/email/templates/email.welcome.template.html @@ -0,0 +1 @@ +

Welcome{name},


Sign up success with email{email}

Support Email:{supportEmail}

Visit us:{clientUrl}


By:{appName}

\ No newline at end of file diff --git a/src/modules/export/export.module.ts b/src/modules/export/export.module.ts new file mode 100644 index 000000000..dd222df39 --- /dev/null +++ b/src/modules/export/export.module.ts @@ -0,0 +1 @@ +// TODO: EXPORT MODULE WITH BULLMQ diff --git a/src/modules/health/controllers/health.private.controller.ts b/src/modules/health/controllers/health.system.controller.ts similarity index 87% rename from src/modules/health/controllers/health.private.controller.ts rename to src/modules/health/controllers/health.system.controller.ts index 0ec48366b..3dd928728 100644 --- a/src/modules/health/controllers/health.private.controller.ts +++ b/src/modules/health/controllers/health.system.controller.ts @@ -8,20 +8,20 @@ import { MongooseHealthIndicator, } from '@nestjs/terminus'; import { Connection } from 'mongoose'; -import { ApiKeyPrivateProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { ApiKeySystemProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; import { Response } from 'src/common/response/decorators/response.decorator'; import { IResponse } from 'src/common/response/interfaces/response.interface'; -import { HealthCheckDoc } from 'src/modules/health/docs/health.doc'; import { HealthResponseDto } from 'src/modules/health/dtos/response/health.response.dto'; import { HealthAwsS3Indicator } from 'src/modules/health/indicators/health.aws-s3.indicator'; +import { HealthSystemCheckDoc } from 'src/modules/health/docs/health.system.doc'; -@ApiTags('modules.private.health') +@ApiTags('modules.system.health') @Controller({ version: VERSION_NEUTRAL, path: '/health', }) -export class HealthPrivateController { +export class HealthSystemController { constructor( @DatabaseConnection() private readonly databaseConnection: Connection, private readonly health: HealthCheckService, @@ -31,10 +31,10 @@ export class HealthPrivateController { private readonly awsS3Indicator: HealthAwsS3Indicator ) {} - @HealthCheckDoc() + @HealthSystemCheckDoc() @Response('health.check') @HealthCheck() - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/aws') async checkAws(): Promise> { const data = await this.health.check([ @@ -46,10 +46,10 @@ export class HealthPrivateController { }; } - @HealthCheckDoc() + @HealthSystemCheckDoc() @Response('health.check') @HealthCheck() - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/database') async checkDatabase(): Promise> { const data = await this.health.check([ @@ -63,10 +63,10 @@ export class HealthPrivateController { }; } - @HealthCheckDoc() + @HealthSystemCheckDoc() @Response('health.check') @HealthCheck() - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/memory-heap') async checkMemoryHeap(): Promise> { const data = await this.health.check([ @@ -82,10 +82,10 @@ export class HealthPrivateController { }; } - @HealthCheckDoc() + @HealthSystemCheckDoc() @Response('health.check') @HealthCheck() - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/memory-rss') async checkMemoryRss(): Promise> { const data = await this.health.check([ @@ -101,10 +101,10 @@ export class HealthPrivateController { }; } - @HealthCheckDoc() + @HealthSystemCheckDoc() @Response('health.check') @HealthCheck() - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/storage') async checkStorage(): Promise> { const data = await this.health.check([ diff --git a/src/modules/health/docs/health.doc.ts b/src/modules/health/docs/health.system.doc.ts similarity index 89% rename from src/modules/health/docs/health.doc.ts rename to src/modules/health/docs/health.system.doc.ts index e87dd6647..41759b6f9 100644 --- a/src/modules/health/docs/health.doc.ts +++ b/src/modules/health/docs/health.system.doc.ts @@ -6,7 +6,7 @@ import { } from 'src/common/doc/decorators/doc.decorator'; import { HealthResponseDto } from 'src/modules/health/dtos/response/health.response.dto'; -export function HealthCheckDoc(): MethodDecorator { +export function HealthSystemCheckDoc(): MethodDecorator { return applyDecorators( Doc({ summary: 'health check api', diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index a04b480f3..c809c46f9 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { AwsModule } from 'src/common/aws/aws.module'; +import { AwsModule } from 'src/modules/aws/aws.module'; import { HealthAwsS3Indicator } from 'src/modules/health/indicators/health.aws-s3.indicator'; @Module({ diff --git a/src/modules/health/indicators/health.aws-s3.indicator.ts b/src/modules/health/indicators/health.aws-s3.indicator.ts index 40b509112..ddd2d92cd 100644 --- a/src/modules/health/indicators/health.aws-s3.indicator.ts +++ b/src/modules/health/indicators/health.aws-s3.indicator.ts @@ -4,7 +4,7 @@ import { HealthIndicator, HealthIndicatorResult, } from '@nestjs/terminus'; -import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; +import { AwsS3Service } from 'src/modules/aws/services/aws.s3.service'; @Injectable() export class HealthAwsS3Indicator extends HealthIndicator { @@ -16,9 +16,9 @@ export class HealthAwsS3Indicator extends HealthIndicator { try { await this.awsS3Service.checkBucketExistence(); return this.getStatus(key, true); - } catch (err: unknown) { + } catch (err: any) { throw new HealthCheckError( - 'HealthAwsS3Indicator failed', + `HealthAwsS3Indicator Failed - ${err?.message}`, this.getStatus(key, false) ); } diff --git a/src/modules/hello/controllers/hello.public.controller.ts b/src/modules/hello/controllers/hello.public.controller.ts index ea243cb03..533dba60e 100644 --- a/src/modules/hello/controllers/hello.public.controller.ts +++ b/src/modules/hello/controllers/hello.public.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; import { Response } from 'src/common/response/decorators/response.decorator'; import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { HelloDoc } from 'src/modules/hello/docs/hello.doc'; import { HelloResponseDto } from 'src/modules/hello/dtos/response/hello.response.dto'; @@ -15,7 +16,10 @@ export class HelloPublicController { constructor(private readonly helperDateService: HelperDateService) {} @HelloDoc() - @Response('hello.hello') + @Response('hello.hello', { + cached: true, + }) + @ApiKeyProtected() @Get('/') async hello(): Promise> { const newDate = this.helperDateService.create(); diff --git a/src/modules/hello/docs/hello.doc.ts b/src/modules/hello/docs/hello.doc.ts index 2e61cf430..d6590fd3d 100644 --- a/src/modules/hello/docs/hello.doc.ts +++ b/src/modules/hello/docs/hello.doc.ts @@ -1,5 +1,9 @@ import { applyDecorators } from '@nestjs/common'; -import { Doc, DocResponse } from 'src/common/doc/decorators/doc.decorator'; +import { + Doc, + DocAuth, + DocResponse, +} from 'src/common/doc/decorators/doc.decorator'; import { HelloResponseDto } from 'src/modules/hello/dtos/response/hello.response.dto'; export function HelloDoc(): MethodDecorator { @@ -7,6 +11,9 @@ export function HelloDoc(): MethodDecorator { Doc({ summary: 'hello test api', }), + DocAuth({ + xApiKey: true, + }), DocResponse('app.hello', { dto: HelloResponseDto, }) diff --git a/src/modules/hello/dtos/response/hello.response.dto.ts b/src/modules/hello/dtos/response/hello.response.dto.ts index 12dcac234..00f4a3e75 100644 --- a/src/modules/hello/dtos/response/hello.response.dto.ts +++ b/src/modules/hello/dtos/response/hello.response.dto.ts @@ -9,19 +9,19 @@ export class HelloResponseDto { example: faker.date.recent(), }) @Type(() => String) - readonly date: Date; + date: Date; @ApiProperty({ required: true, nullable: false, example: faker.date.recent(), }) - readonly format: string; + format: string; @ApiProperty({ required: true, nullable: false, example: 1660190937231, }) - readonly timestamp: number; + timestamp: number; } diff --git a/src/common/policy/constants/policy.constant.ts b/src/modules/policy/constants/policy.constant.ts similarity index 100% rename from src/common/policy/constants/policy.constant.ts rename to src/modules/policy/constants/policy.constant.ts diff --git a/src/common/policy/decorators/policy.decorator.ts b/src/modules/policy/decorators/policy.decorator.ts similarity index 60% rename from src/common/policy/decorators/policy.decorator.ts rename to src/modules/policy/decorators/policy.decorator.ts index c9928b705..47677760f 100644 --- a/src/common/policy/decorators/policy.decorator.ts +++ b/src/modules/policy/decorators/policy.decorator.ts @@ -2,11 +2,11 @@ import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common'; import { POLICY_ABILITY_META_KEY, POLICY_ROLE_META_KEY, -} from 'src/common/policy/constants/policy.constant'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; -import { PolicyAbilityGuard } from 'src/common/policy/guards/policy.ability.guard'; -import { PolicyRoleGuard } from 'src/common/policy/guards/policy.role.guard'; -import { IPolicyAbility } from 'src/common/policy/interfaces/policy.interface'; +} from 'src/modules/policy/constants/policy.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; +import { PolicyAbilityGuard } from 'src/modules/policy/guards/policy.ability.guard'; +import { PolicyRoleGuard } from 'src/modules/policy/guards/policy.role.guard'; +import { IPolicyAbility } from 'src/modules/policy/interfaces/policy.interface'; export function PolicyAbilityProtected( ...handlers: IPolicyAbility[] diff --git a/src/common/policy/constants/policy.enum.constant.ts b/src/modules/policy/enums/policy.enum.ts similarity index 96% rename from src/common/policy/constants/policy.enum.constant.ts rename to src/modules/policy/enums/policy.enum.ts index b7cb060cf..d24fecb64 100644 --- a/src/common/policy/constants/policy.enum.constant.ts +++ b/src/modules/policy/enums/policy.enum.ts @@ -20,6 +20,7 @@ export enum ENUM_POLICY_REQUEST_ACTION { export enum ENUM_POLICY_SUBJECT { ALL = 'ALL', + AUTH = 'AUTH', API_KEY = 'API_KEY', SETTING = 'SETTING', COUNTRY = 'COUNTRY', diff --git a/src/modules/policy/enums/policy.status-code.enum.ts b/src/modules/policy/enums/policy.status-code.enum.ts new file mode 100644 index 000000000..9dd90dd93 --- /dev/null +++ b/src/modules/policy/enums/policy.status-code.enum.ts @@ -0,0 +1,6 @@ +export enum ENUM_POLICY_STATUS_CODE_ERROR { + ABILITY_FORBIDDEN = 5010, + ROLE_FORBIDDEN = 5011, + ABILITY_PREDEFINED_NOT_FOUND = 5012, + ROLE_PREDEFINED_NOT_FOUND = 5013, +} diff --git a/src/common/policy/factories/policy.factory.ts b/src/modules/policy/factories/policy.factory.ts similarity index 92% rename from src/common/policy/factories/policy.factory.ts rename to src/modules/policy/factories/policy.factory.ts index c81058ec0..bf0f8c447 100644 --- a/src/common/policy/factories/policy.factory.ts +++ b/src/modules/policy/factories/policy.factory.ts @@ -1,17 +1,17 @@ import { AbilityBuilder, createMongoAbility } from '@casl/ability'; import { Injectable } from '@nestjs/common'; -import { AuthJwtAccessPayloadPermissionDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtAccessPayloadPermissionDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; import { ENUM_POLICY_ACTION, ENUM_POLICY_REQUEST_ACTION, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; import { IPolicyAbility, IPolicyAbilityFlat, IPolicyAbilityHandlerCallback, IPolicyAbilityRule, -} from 'src/common/policy/interfaces/policy.interface'; +} from 'src/modules/policy/interfaces/policy.interface'; @Injectable() export class PolicyAbilityFactory { diff --git a/src/common/policy/guards/policy.ability.guard.ts b/src/modules/policy/guards/policy.ability.guard.ts similarity index 68% rename from src/common/policy/guards/policy.ability.guard.ts rename to src/modules/policy/guards/policy.ability.guard.ts index 459cc186a..23791d294 100644 --- a/src/common/policy/guards/policy.ability.guard.ts +++ b/src/modules/policy/guards/policy.ability.guard.ts @@ -6,14 +6,14 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/common/policy/constants/policy.status-code.constant'; -import { PolicyAbilityFactory } from 'src/common/policy/factories/policy.factory'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; +import { PolicyAbilityFactory } from 'src/modules/policy/factories/policy.factory'; import { IPolicyAbility, IPolicyAbilityHandlerCallback, -} from 'src/common/policy/interfaces/policy.interface'; -import { POLICY_ABILITY_META_KEY } from 'src/common/policy/constants/policy.constant'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/interfaces/policy.interface'; +import { POLICY_ABILITY_META_KEY } from 'src/modules/policy/constants/policy.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; @Injectable() export class PolicyAbilityGuard implements CanActivate { @@ -34,6 +34,12 @@ export class PolicyAbilityGuard implements CanActivate { if (type === ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN) { return true; + } else if (policy.length === 0) { + throw new ForbiddenException({ + statusCode: + ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_PREDEFINED_NOT_FOUND, + message: 'policy.error.abilityPredefinedNotFound', + }); } const ability = @@ -49,8 +55,7 @@ export class PolicyAbilityGuard implements CanActivate { if (!check) { throw new ForbiddenException({ - statusCode: - ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN_ERROR, + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN, message: 'policy.error.abilityForbidden', }); } diff --git a/src/common/policy/guards/policy.role.guard.ts b/src/modules/policy/guards/policy.role.guard.ts similarity index 59% rename from src/common/policy/guards/policy.role.guard.ts rename to src/modules/policy/guards/policy.role.guard.ts index c53b78bdd..0d67b4fba 100644 --- a/src/common/policy/guards/policy.role.guard.ts +++ b/src/modules/policy/guards/policy.role.guard.ts @@ -5,9 +5,9 @@ import { Injectable, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { POLICY_ROLE_META_KEY } from 'src/common/policy/constants/policy.constant'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; -import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/common/policy/constants/policy.status-code.constant'; +import { POLICY_ROLE_META_KEY } from 'src/modules/policy/constants/policy.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; @Injectable() @@ -24,13 +24,17 @@ export class PolicyRoleGuard implements CanActivate { const { user } = context.switchToHttp().getRequest(); const { type } = user; - if (role?.length === 0 || type === ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN) { + if (type === ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN) { return true; - } - - if (!role.includes(type)) { + } else if (role.length === 0) { + throw new ForbiddenException({ + statusCode: + ENUM_POLICY_STATUS_CODE_ERROR.ROLE_PREDEFINED_NOT_FOUND, + message: 'policy.error.rolePredefinedNotFound', + }); + } else if (!role.includes(type)) { throw new ForbiddenException({ - statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN_ERROR, + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN, message: 'policy.error.roleForbidden', }); } diff --git a/src/common/policy/interfaces/policy.interface.ts b/src/modules/policy/interfaces/policy.interface.ts similarity index 91% rename from src/common/policy/interfaces/policy.interface.ts rename to src/modules/policy/interfaces/policy.interface.ts index 72a86a480..8fc94d404 100644 --- a/src/common/policy/interfaces/policy.interface.ts +++ b/src/modules/policy/interfaces/policy.interface.ts @@ -2,7 +2,7 @@ import { InferSubjects, MongoAbility } from '@casl/ability'; import { ENUM_POLICY_ACTION, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; export interface IPolicyAbility { subject: ENUM_POLICY_SUBJECT; diff --git a/src/common/policy/policy.module.ts b/src/modules/policy/policy.module.ts similarity index 80% rename from src/common/policy/policy.module.ts rename to src/modules/policy/policy.module.ts index 0a5f8ca94..bd8deb3c8 100644 --- a/src/common/policy/policy.module.ts +++ b/src/modules/policy/policy.module.ts @@ -1,5 +1,5 @@ import { DynamicModule, Global, Module } from '@nestjs/common'; -import { PolicyAbilityFactory } from 'src/common/policy/factories/policy.factory'; +import { PolicyAbilityFactory } from 'src/modules/policy/factories/policy.factory'; @Global() @Module({}) diff --git a/src/modules/reset-password/reset-password.module.ts b/src/modules/reset-password/reset-password.module.ts new file mode 100644 index 000000000..94f9664fa --- /dev/null +++ b/src/modules/reset-password/reset-password.module.ts @@ -0,0 +1 @@ +// TODO: RESET PASSWORD MODULE diff --git a/src/modules/role/constants/role.doc.constant.ts b/src/modules/role/constants/role.doc.constant.ts index 3aaa07109..54845c74b 100644 --- a/src/modules/role/constants/role.doc.constant.ts +++ b/src/modules/role/constants/role.doc.constant.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; export const RoleDocQueryIsActive = [ { diff --git a/src/modules/role/constants/role.list.constant.ts b/src/modules/role/constants/role.list.constant.ts index e48c57373..efb84eac7 100644 --- a/src/modules/role/constants/role.list.constant.ts +++ b/src/modules/role/constants/role.list.constant.ts @@ -1,4 +1,4 @@ -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; export const ROLE_DEFAULT_AVAILABLE_SEARCH = ['name']; export const ROLE_DEFAULT_IS_ACTIVE = [true, false]; diff --git a/src/modules/role/constants/role.status-code.constant.ts b/src/modules/role/constants/role.status-code.constant.ts deleted file mode 100644 index fa3a78111..000000000 --- a/src/modules/role/constants/role.status-code.constant.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ENUM_ROLE_STATUS_CODE_ERROR { - NOT_FOUND_ERROR = 5100, - EXIST_ERROR = 5101, - IS_ACTIVE_ERROR = 5102, - USED_ERROR = 5103, -} diff --git a/src/modules/role/controllers/role.admin.controller.ts b/src/modules/role/controllers/role.admin.controller.ts index ae11e8ae3..711c84567 100644 --- a/src/modules/role/controllers/role.admin.controller.ts +++ b/src/modules/role/controllers/role.admin.controller.ts @@ -2,7 +2,6 @@ import { Body, ConflictException, Controller, - Delete, Get, Param, Patch, @@ -10,9 +9,6 @@ import { Put, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; import { PaginationQuery, PaginationQueryFilterInBoolean, @@ -20,15 +16,6 @@ import { } from 'src/common/pagination/decorators/pagination.decorator'; import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; -import { - ENUM_POLICY_ACTION, - ENUM_POLICY_ROLE_TYPE, - ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; -import { - PolicyAbilityProtected, - PolicyRoleProtected, -} from 'src/common/policy/decorators/policy.decorator'; import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; import { Response, @@ -38,16 +25,26 @@ import { IResponse, IResponsePaging, } from 'src/common/response/interfaces/response.interface'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { + PolicyAbilityProtected, + PolicyRoleProtected, +} from 'src/modules/policy/decorators/policy.decorator'; import { ROLE_DEFAULT_AVAILABLE_SEARCH, ROLE_DEFAULT_IS_ACTIVE, ROLE_DEFAULT_POLICY_ROLE_TYPE, } from 'src/modules/role/constants/role.list.constant'; -import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/enums/role.status-code.enum'; import { RoleAdminActiveDoc, RoleAdminCreateDoc, - RoleAdminDeleteDoc, RoleAdminGetDoc, RoleAdminInactiveDoc, RoleAdminListDoc, @@ -57,15 +54,12 @@ import { RoleCreateRequestDto } from 'src/modules/role/dtos/request/role.create. import { RoleUpdateRequestDto } from 'src/modules/role/dtos/request/role.update.request.dto'; import { RoleGetResponseDto } from 'src/modules/role/dtos/response/role.get.response.dto'; import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; -import { - RoleActivePipe, - RoleInactivePipe, -} from 'src/modules/role/pipes/role.is-active.pipe'; +import { RoleIsActivePipe } from 'src/modules/role/pipes/role.is-active.pipe'; import { RoleParsePipe } from 'src/modules/role/pipes/role.parse.pipe'; import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; import { RoleService } from 'src/modules/role/services/role.service'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; import { UserService } from 'src/modules/user/services/user.service'; +import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; @ApiTags('modules.admin.role') @Controller({ @@ -87,7 +81,7 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/list') async list( @PaginationQuery({ availableSearch: ROLE_DEFAULT_AVAILABLE_SEARCH }) @@ -137,7 +131,7 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('get/:role') async get( @Param('role', RequestRequiredPipe, RoleParsePipe) role: RoleDoc @@ -155,7 +149,7 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Post('/create') async create( @Body() @@ -164,7 +158,7 @@ export class RoleAdminController { const exist: boolean = await this.roleService.existByName(name); if (exist) { throw new ConflictException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.EXIST_ERROR, + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.EXIST, message: 'role.error.exist', }); } @@ -189,7 +183,7 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Put('/update/:role') async update( @Param('role', RequestRequiredPipe, RoleParsePipe) role: RoleDoc, @@ -203,34 +197,6 @@ export class RoleAdminController { }; } - @RoleAdminDeleteDoc() - @Response('role.delete') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.ROLE, - action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.DELETE], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Delete('/delete/:role') - async delete( - @Param('role', RequestRequiredPipe, RoleParsePipe) role: RoleDoc - ): Promise { - const used: UserDoc = await this.userService.findOne({ - role: role._id, - }); - if (used) { - throw new ConflictException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.USED_ERROR, - message: 'role.error.used', - }); - } - - await this.roleService.delete(role); - - return; - } - @RoleAdminInactiveDoc() @Response('role.inactive') @PolicyAbilityProtected({ @@ -239,10 +205,15 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:role/inactive') async inactive( - @Param('role', RequestRequiredPipe, RoleParsePipe, RoleActivePipe) + @Param( + 'role', + RequestRequiredPipe, + RoleParsePipe, + new RoleIsActivePipe([true]) + ) role: RoleDoc ): Promise { await this.roleService.inactive(role); @@ -258,10 +229,15 @@ export class RoleAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:role/active') async active( - @Param('role', RequestRequiredPipe, RoleParsePipe, RoleInactivePipe) + @Param( + 'role', + RequestRequiredPipe, + RoleParsePipe, + new RoleIsActivePipe([false]) + ) role: RoleDoc ): Promise { await this.roleService.active(role); diff --git a/src/modules/role/controllers/role.system.controller.ts b/src/modules/role/controllers/role.system.controller.ts new file mode 100644 index 000000000..435455fc0 --- /dev/null +++ b/src/modules/role/controllers/role.system.controller.ts @@ -0,0 +1,73 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiKeySystemProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { + PaginationQuery, + PaginationQueryFilterInEnum, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; +import { ResponsePaging } from 'src/common/response/decorators/response.decorator'; +import { IResponsePaging } from 'src/common/response/interfaces/response.interface'; +import { + ROLE_DEFAULT_AVAILABLE_SEARCH, + ROLE_DEFAULT_POLICY_ROLE_TYPE, +} from 'src/modules/role/constants/role.list.constant'; +import { RoleShortResponseDto } from 'src/modules/role/dtos/response/role.short.response.dto'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleSystemListDoc } from 'src/modules/role/docs/role.system.controller'; + +@ApiTags('modules.system.role') +@Controller({ + version: '1', + path: '/role', +}) +export class RoleSystemController { + constructor( + private readonly paginationService: PaginationService, + private readonly roleService: RoleService + ) {} + + @RoleSystemListDoc() + @ResponsePaging('role.list') + @ApiKeySystemProtected() + @Get('/list') + async list( + @PaginationQuery({ availableSearch: ROLE_DEFAULT_AVAILABLE_SEARCH }) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInEnum( + 'type', + ROLE_DEFAULT_POLICY_ROLE_TYPE, + ENUM_POLICY_ROLE_TYPE + ) + type: Record + ): Promise> { + const find: Record = { + ..._search, + ...type, + }; + + const roles: RoleDoc[] = await this.roleService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + + const total: number = await this.roleService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + const mapRoles: RoleShortResponseDto[] = + await this.roleService.mapShort(roles); + + return { + _pagination: { total, totalPage }, + data: mapRoles, + }; + } +} diff --git a/src/modules/role/docs/role.admin.doc.ts b/src/modules/role/docs/role.admin.doc.ts index 21419ebc3..c3f17ccfe 100644 --- a/src/modules/role/docs/role.admin.doc.ts +++ b/src/modules/role/docs/role.admin.doc.ts @@ -1,6 +1,5 @@ import { applyDecorators, HttpStatus } from '@nestjs/common'; import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { Doc, DocAuth, @@ -9,6 +8,7 @@ import { DocResponse, DocResponsePaging, } from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; import { RoleDocParamsId, RoleDocQueryIsActive, @@ -132,20 +132,3 @@ export function RoleAdminUpdateDoc(): MethodDecorator { }) ); } - -export function RoleAdminDeleteDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'delete a role', - }), - DocRequest({ - params: RoleDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponse('role.delete') - ); -} diff --git a/src/modules/role/docs/role.system.controller.ts b/src/modules/role/docs/role.system.controller.ts new file mode 100644 index 000000000..aff6852b1 --- /dev/null +++ b/src/modules/role/docs/role.system.controller.ts @@ -0,0 +1,26 @@ +import { applyDecorators } from '@nestjs/common'; +import { + Doc, + DocAuth, + DocRequest, + DocResponsePaging, +} from 'src/common/doc/decorators/doc.decorator'; +import { RoleDocQueryType } from 'src/modules/role/constants/role.doc.constant'; +import { RoleShortResponseDto } from 'src/modules/role/dtos/response/role.short.response.dto'; + +export function RoleSystemListDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'get all of roles', + }), + DocRequest({ + queries: RoleDocQueryType, + }), + DocAuth({ + xApiKey: true, + }), + DocResponsePaging('role.list', { + dto: RoleShortResponseDto, + }) + ); +} diff --git a/src/modules/role/dtos/request/role.create.request.dto.ts b/src/modules/role/dtos/request/role.create.request.dto.ts index 3f55beb1e..520fcde15 100644 --- a/src/modules/role/dtos/request/role.create.request.dto.ts +++ b/src/modules/role/dtos/request/role.create.request.dto.ts @@ -23,5 +23,5 @@ export class RoleCreateRequestDto extends IntersectionType( @MinLength(3) @MaxLength(30) @Type(() => String) - readonly name: string; + name: string; } diff --git a/src/modules/role/dtos/request/role.update.request.dto.ts b/src/modules/role/dtos/request/role.update.request.dto.ts index 37fb2cac1..06f2dd5c0 100644 --- a/src/modules/role/dtos/request/role.update.request.dto.ts +++ b/src/modules/role/dtos/request/role.update.request.dto.ts @@ -10,12 +10,12 @@ import { ValidateNested, } from 'class-validator'; import { Transform, Type } from 'class-transformer'; +import { RolePermissionDto } from 'src/modules/role/dtos/role.permission.dto'; import { ENUM_POLICY_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; -import { RolePermissionDto } from 'src/modules/role/dtos/role.permission.dto'; +} from 'src/modules/policy/enums/policy.enum'; export class RoleUpdateRequestDto { @ApiProperty({ @@ -27,7 +27,7 @@ export class RoleUpdateRequestDto { @IsString() @IsOptional() @Type(() => String) - readonly description?: string; + description?: string; @ApiProperty({ description: 'Representative for role type', @@ -36,7 +36,7 @@ export class RoleUpdateRequestDto { }) @IsEnum(ENUM_POLICY_ROLE_TYPE) @IsNotEmpty() - readonly type: ENUM_POLICY_ROLE_TYPE; + type: ENUM_POLICY_ROLE_TYPE; @ApiProperty({ required: true, @@ -59,5 +59,5 @@ export class RoleUpdateRequestDto { @Transform(({ value, obj }) => obj.type !== ENUM_POLICY_ROLE_TYPE.ADMIN ? [] : value ) - readonly permissions: RolePermissionDto[]; + permissions: RolePermissionDto[]; } diff --git a/src/modules/role/dtos/response/role.get.response.dto.ts b/src/modules/role/dtos/response/role.get.response.dto.ts index 2a62865a7..a230b363b 100644 --- a/src/modules/role/dtos/response/role.get.response.dto.ts +++ b/src/modules/role/dtos/response/role.get.response.dto.ts @@ -1,18 +1,18 @@ import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude, Type } from 'class-transformer'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; import { RolePermissionDto } from 'src/modules/role/dtos/role.permission.dto'; -export class RoleGetResponseDto extends DatabaseIdResponseDto { +export class RoleGetResponseDto extends DatabaseDto { @ApiProperty({ description: 'Name of role', example: faker.person.jobTitle(), required: true, nullable: false, }) - readonly name: string; + name: string; @ApiProperty({ description: 'Description of role', @@ -20,7 +20,7 @@ export class RoleGetResponseDto extends DatabaseIdResponseDto { required: false, nullable: true, }) - readonly description?: string; + description?: string; @ApiProperty({ description: 'Active flag of role', @@ -28,7 +28,7 @@ export class RoleGetResponseDto extends DatabaseIdResponseDto { required: true, nullable: false, }) - readonly isActive: boolean; + isActive: boolean; @ApiProperty({ description: 'Representative for role type', @@ -36,7 +36,7 @@ export class RoleGetResponseDto extends DatabaseIdResponseDto { required: true, nullable: false, }) - readonly type: ENUM_POLICY_ROLE_TYPE; + type: ENUM_POLICY_ROLE_TYPE; @ApiProperty({ type: RolePermissionDto, @@ -45,25 +45,5 @@ export class RoleGetResponseDto extends DatabaseIdResponseDto { default: [], }) @Type(() => RolePermissionDto) - readonly permissions: RolePermissionDto; - - @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; + permissions: RolePermissionDto; } diff --git a/src/modules/role/dtos/response/role.list.response.dto.ts b/src/modules/role/dtos/response/role.list.response.dto.ts index 7b85912f6..6d24e330f 100644 --- a/src/modules/role/dtos/response/role.list.response.dto.ts +++ b/src/modules/role/dtos/response/role.list.response.dto.ts @@ -1,14 +1,19 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { ApiHideProperty, ApiProperty, OmitType } from '@nestjs/swagger'; +import { Exclude, Transform } from 'class-transformer'; import { RoleGetResponseDto } from 'src/modules/role/dtos/response/role.get.response.dto'; export class RoleListResponseDto extends OmitType(RoleGetResponseDto, [ 'permissions', + 'description', ] as const) { + @ApiHideProperty() + @Exclude() + description?: string; + @ApiProperty({ description: 'count of permissions', required: true, }) @Transform(({ value }) => value.length) - readonly permissions: number; + permissions: number; } diff --git a/src/modules/role/dtos/response/role.short.response.dto.ts b/src/modules/role/dtos/response/role.short.response.dto.ts new file mode 100644 index 000000000..85a66eb03 --- /dev/null +++ b/src/modules/role/dtos/response/role.short.response.dto.ts @@ -0,0 +1,26 @@ +import { ApiHideProperty, OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; + +export class RoleShortResponseDto extends OmitType(RoleListResponseDto, [ + 'permissions', + 'isActive', + 'createdAt', + 'updatedAt', +] as const) { + @ApiHideProperty() + @Exclude() + permissions: number; + + @ApiHideProperty() + @Exclude() + isActive: boolean; + + @ApiHideProperty() + @Exclude() + createdAt: Date; + + @ApiHideProperty() + @Exclude() + updatedAt: Date; +} diff --git a/src/modules/role/dtos/role.permission.dto.ts b/src/modules/role/dtos/role.permission.dto.ts index 18f94b53c..ef159320b 100644 --- a/src/modules/role/dtos/role.permission.dto.ts +++ b/src/modules/role/dtos/role.permission.dto.ts @@ -9,7 +9,7 @@ import { import { ENUM_POLICY_ACTION, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; export class RolePermissionDto { @ApiProperty({ diff --git a/src/modules/role/enums/role.status-code.enum.ts b/src/modules/role/enums/role.status-code.enum.ts new file mode 100644 index 000000000..cb2466aa7 --- /dev/null +++ b/src/modules/role/enums/role.status-code.enum.ts @@ -0,0 +1,7 @@ +export enum ENUM_ROLE_STATUS_CODE_ERROR { + NOT_FOUND = 5100, + EXIST = 5101, + IS_ACTIVE = 5102, + USED = 5103, + INACTIVE_FORBIDDEN = 5104, +} diff --git a/src/modules/role/interfaces/role.service.interface.ts b/src/modules/role/interfaces/role.service.interface.ts index bee9c08ff..6f7a2badd 100644 --- a/src/modules/role/interfaces/role.service.interface.ts +++ b/src/modules/role/interfaces/role.service.interface.ts @@ -1,38 +1,50 @@ import { IDatabaseCreateManyOptions, IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseExistOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, } from 'src/common/database/interfaces/database.interface'; import { RoleCreateRequestDto } from 'src/modules/role/dtos/request/role.create.request.dto'; import { RoleUpdateRequestDto } from 'src/modules/role/dtos/request/role.update.request.dto'; -import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleGetResponseDto } from 'src/modules/role/dtos/response/role.get.response.dto'; +import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; +import { RoleShortResponseDto } from 'src/modules/role/dtos/response/role.short.response.dto'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; export interface IRoleService { findAll( find?: Record, options?: IDatabaseFindAllOptions ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByName( - name: string, - options?: IDatabaseFindOneOptions - ): Promise; getTotal( find?: Record, options?: IDatabaseGetTotalOptions ): Promise; + findAllActive( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + getTotalActive( + find?: Record, + options?: IDatabaseGetTotalOptions + ): Promise; + findOneById(_id: string, options?: IDatabaseOptions): Promise; + findOne( + find: Record, + options?: IDatabaseOptions + ): Promise; + findOneByName(name: string, options?: IDatabaseOptions): Promise; + findOneActiveById( + _id: string, + options?: IDatabaseOptions + ): Promise; existByName( name: string, options?: IDatabaseExistOptions @@ -54,16 +66,15 @@ export interface IRoleService { repository: RoleDoc, options?: IDatabaseSaveOptions ): Promise; - delete( - repository: RoleDoc, - options?: IDatabaseSaveOptions - ): Promise; deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise; createMany( data: RoleCreateRequestDto[], options?: IDatabaseCreateManyOptions ): Promise; + mapList(roles: RoleDoc[] | RoleEntity[]): Promise; + mapGet(role: RoleDoc | RoleEntity): Promise; + mapShort(roles: RoleDoc[] | RoleEntity[]): Promise; } diff --git a/src/modules/role/pipes/role.is-active.pipe.ts b/src/modules/role/pipes/role.is-active.pipe.ts index 9b11624ed..36b8ec705 100644 --- a/src/modules/role/pipes/role.is-active.pipe.ts +++ b/src/modules/role/pipes/role.is-active.pipe.ts @@ -1,27 +1,19 @@ import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; -import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/enums/role.status-code.enum'; import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; @Injectable() -export class RoleActivePipe implements PipeTransform { - async transform(value: RoleDoc): Promise { - if (!value.isActive) { - throw new BadRequestException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, - message: 'role.error.isActiveInvalid', - }); - } +export class RoleIsActivePipe implements PipeTransform { + private readonly isActive: boolean[]; - return value; + constructor(isActive: boolean[]) { + this.isActive = isActive; } -} -@Injectable() -export class RoleInactivePipe implements PipeTransform { async transform(value: RoleDoc): Promise { - if (value.isActive) { + if (!this.isActive.includes(value.isActive)) { throw new BadRequestException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.IS_ACTIVE_ERROR, + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.IS_ACTIVE, message: 'role.error.isActiveInvalid', }); } diff --git a/src/modules/role/pipes/role.parse.pipe.ts b/src/modules/role/pipes/role.parse.pipe.ts index 1c02d6dc8..bcdb1e0d5 100644 --- a/src/modules/role/pipes/role.parse.pipe.ts +++ b/src/modules/role/pipes/role.parse.pipe.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; -import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/enums/role.status-code.enum'; import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; import { RoleService } from 'src/modules/role/services/role.service'; @@ -11,7 +11,7 @@ export class RoleParsePipe implements PipeTransform { const role: RoleDoc = await this.roleService.findOneById(value); if (!role) { throw new NotFoundException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND, message: 'role.error.notFound', }); } diff --git a/src/modules/role/repository/entities/role.entity.ts b/src/modules/role/repository/entities/role.entity.ts index 31f0ac8eb..ca52dbe2b 100644 --- a/src/modules/role/repository/entities/role.entity.ts +++ b/src/modules/role/repository/entities/role.entity.ts @@ -1,11 +1,11 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; import { RolePermissionEntity, RolePermissionSchema, @@ -14,7 +14,7 @@ import { export const RoleTableName = 'Roles'; @DatabaseEntity({ collection: RoleTableName }) -export class RoleEntity extends DatabaseMongoUUIDEntityAbstract { +export class RoleEntity extends DatabaseEntityAbstract { @DatabaseProp({ required: true, index: true, diff --git a/src/modules/role/repository/entities/role.permission.entity.ts b/src/modules/role/repository/entities/role.permission.entity.ts index 4536fb6da..ef722a8de 100644 --- a/src/modules/role/repository/entities/role.permission.entity.ts +++ b/src/modules/role/repository/entities/role.permission.entity.ts @@ -6,7 +6,7 @@ import { import { ENUM_POLICY_ACTION, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; @DatabaseEntity({ timestamps: false, _id: false }) export class RolePermissionEntity { diff --git a/src/modules/role/repository/repositories/role.repository.ts b/src/modules/role/repository/repositories/role.repository.ts index 804b68b19..1fa2b1eb4 100644 --- a/src/modules/role/repository/repositories/role.repository.ts +++ b/src/modules/role/repository/repositories/role.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/database.repository.abstract'; import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; import { RoleDoc, @@ -8,7 +8,7 @@ import { } from 'src/modules/role/repository/entities/role.entity'; @Injectable() -export class RoleRepository extends DatabaseMongoUUIDRepositoryAbstract< +export class RoleRepository extends DatabaseRepositoryAbstract< RoleEntity, RoleDoc > { diff --git a/src/modules/role/services/role.service.ts b/src/modules/role/services/role.service.ts index 8177a3dfc..fa90196dd 100644 --- a/src/modules/role/services/role.service.ts +++ b/src/modules/role/services/role.service.ts @@ -1,19 +1,21 @@ import { Injectable } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; +import { Document } from 'mongoose'; import { IDatabaseCreateOptions, IDatabaseExistOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, IDatabaseCreateManyOptions, IDatabaseSaveOptions, + IDatabaseOptions, + IDatabaseDeleteManyOptions, } from 'src/common/database/interfaces/database.interface'; import { RoleCreateRequestDto } from 'src/modules/role/dtos/request/role.create.request.dto'; import { RoleUpdateRequestDto } from 'src/modules/role/dtos/request/role.update.request.dto'; import { RoleGetResponseDto } from 'src/modules/role/dtos/response/role.get.response.dto'; import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; +import { RoleShortResponseDto } from 'src/modules/role/dtos/response/role.short.response.dto'; import { IRoleService } from 'src/modules/role/interfaces/role.service.interface'; import { RoleDoc, @@ -29,35 +31,62 @@ export class RoleService implements IRoleService { find?: Record, options?: IDatabaseFindAllOptions ): Promise { - return this.roleRepository.findAll(find, options); + return this.roleRepository.findAll(find, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseGetTotalOptions + ): Promise { + return this.roleRepository.getTotal(find, options); + } + + async findAllActive( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.roleRepository.findAll( + { ...find, isActive: true }, + options + ); + } + + async getTotalActive( + find?: Record, + options?: IDatabaseGetTotalOptions + ): Promise { + return this.roleRepository.getTotal( + { ...find, isActive: true }, + options + ); } async findOneById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.roleRepository.findOneById(_id, options); + return this.roleRepository.findOneById(_id, options); } async findOne( find: Record, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.roleRepository.findOne(find, options); + return this.roleRepository.findOne(find, options); } async findOneByName( name: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.roleRepository.findOne({ name }, options); + return this.roleRepository.findOne({ name }, options); } - async getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.roleRepository.getTotal(find, options); + async findOneActiveById( + _id: string, + options?: IDatabaseOptions + ): Promise { + return this.roleRepository.findOne({ _id, isActive: true }, options); } async existByName( @@ -116,43 +145,70 @@ export class RoleService implements IRoleService { return this.roleRepository.save(repository, options); } - async delete( - repository: RoleDoc, - options?: IDatabaseSaveOptions - ): Promise { - return this.roleRepository.delete(repository, options); - } - async deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise { - return this.roleRepository.deleteMany(find, options); + try { + await this.roleRepository.deleteMany(find, options); + + return true; + } catch (error: unknown) { + throw error; + } } async createMany( data: RoleCreateRequestDto[], options?: IDatabaseCreateManyOptions ): Promise { - const create: RoleEntity[] = data.map(({ type, name, permissions }) => { - const entity: RoleEntity = new RoleEntity(); - entity.type = type; - entity.isActive = true; - entity.name = name; - entity.permissions = permissions; - - return entity; - }); - return this.roleRepository.createMany(create, options); + try { + const create: RoleEntity[] = data.map( + ({ type, name, permissions }) => { + const entity: RoleEntity = new RoleEntity(); + entity.type = type; + entity.isActive = true; + entity.name = name; + entity.permissions = permissions; + + return entity; + } + ) as RoleEntity[]; + + await this.roleRepository.createMany(create, options); + + return true; + } catch (error: unknown) { + throw error; + } } - async mapList(roles: RoleDoc[]): Promise { - const plainObject: RoleEntity[] = roles.map(e => e.toObject()); + async mapList( + roles: RoleDoc[] | RoleEntity[] + ): Promise { + return plainToInstance( + RoleListResponseDto, + roles.map((e: RoleDoc | RoleEntity) => + e instanceof Document ? e.toObject() : e + ) + ); + } - return plainToInstance(RoleListResponseDto, plainObject); + async mapGet(role: RoleDoc | RoleEntity): Promise { + return plainToInstance( + RoleGetResponseDto, + role instanceof Document ? role.toObject() : role + ); } - async mapGet(role: RoleDoc): Promise { - return plainToInstance(RoleGetResponseDto, role.toObject()); + async mapShort( + roles: RoleDoc[] | RoleEntity[] + ): Promise { + return plainToInstance( + RoleShortResponseDto, + roles.map((e: RoleDoc | RoleEntity) => + e instanceof Document ? e.toObject() : e + ) + ); } } diff --git a/src/modules/session/session.module.ts b/src/modules/session/session.module.ts new file mode 100644 index 000000000..435c3cd9b --- /dev/null +++ b/src/modules/session/session.module.ts @@ -0,0 +1 @@ +// TODO: SESSION MODULE diff --git a/src/modules/setting/constants/setting.status-code.constant.ts b/src/modules/setting/constants/setting.status-code.constant.ts deleted file mode 100644 index 112688bf6..000000000 --- a/src/modules/setting/constants/setting.status-code.constant.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ENUM_SETTING_STATUS_CODE_ERROR { - NOT_FOUND_ERROR = 5130, - VALUE_NOT_ALLOWED_ERROR = 5131, -} diff --git a/src/modules/setting/controllers/setting.admin.controller.ts b/src/modules/setting/controllers/setting.admin.controller.ts index 547d810a4..6ac56a8df 100644 --- a/src/modules/setting/controllers/setting.admin.controller.ts +++ b/src/modules/setting/controllers/setting.admin.controller.ts @@ -7,21 +7,9 @@ import { Put, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; import { PaginationQuery } from 'src/common/pagination/decorators/pagination.decorator'; import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; import { PaginationService } from 'src/common/pagination/services/pagination.service'; -import { - ENUM_POLICY_ACTION, - ENUM_POLICY_ROLE_TYPE, - ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; -import { - PolicyAbilityProtected, - PolicyRoleProtected, -} from 'src/common/policy/decorators/policy.decorator'; import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; import { Response, @@ -31,8 +19,19 @@ import { IResponse, IResponsePaging, } from 'src/common/response/interfaces/response.interface'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { + PolicyAbilityProtected, + PolicyRoleProtected, +} from 'src/modules/policy/decorators/policy.decorator'; import { SETTING_DEFAULT_AVAILABLE_SEARCH } from 'src/modules/setting/constants/setting.list.constant'; -import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/constants/setting.status-code.constant'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/enums/setting.status-code.enum'; import { SettingAdminGetDoc, SettingAdminListDoc, @@ -44,6 +43,7 @@ import { SettingListResponseDto } from 'src/modules/setting/dtos/response/settin import { SettingParsePipe } from 'src/modules/setting/pipes/setting.parse.pipe'; import { SettingDoc } from 'src/modules/setting/repository/entities/setting.entity'; import { SettingService } from 'src/modules/setting/services/setting.service'; +import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; @ApiTags('modules.admin.setting') @Controller({ @@ -64,7 +64,7 @@ export class SettingAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/list') async list( @PaginationQuery({ @@ -132,8 +132,7 @@ export class SettingAdminController { const check = this.settingService.checkValue(setting.type, body.value); if (!check) { throw new BadRequestException({ - statusCode: - ENUM_SETTING_STATUS_CODE_ERROR.VALUE_NOT_ALLOWED_ERROR, + statusCode: ENUM_SETTING_STATUS_CODE_ERROR.VALUE_NOT_ALLOWED, message: 'setting.error.valueNotAllowed', }); } diff --git a/src/modules/setting/controllers/setting.private.controller.ts b/src/modules/setting/controllers/setting.system.controller.ts similarity index 85% rename from src/modules/setting/controllers/setting.private.controller.ts rename to src/modules/setting/controllers/setting.system.controller.ts index 363b1d589..985eccec8 100644 --- a/src/modules/setting/controllers/setting.private.controller.ts +++ b/src/modules/setting/controllers/setting.system.controller.ts @@ -1,32 +1,32 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ApiKeyPrivateProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { ApiKeySystemProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { FILE_SIZE_IN_BYTES } from 'src/common/file/constants/file.constant'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; import { MessageService } from 'src/common/message/services/message.service'; import { Response } from 'src/common/response/decorators/response.decorator'; import { IResponse } from 'src/common/response/interfaces/response.interface'; -import { SettingPrivateCoreDoc } from 'src/modules/setting/docs/setting.private.doc'; import { SettingCoreResponseDto } from 'src/modules/setting/dtos/response/setting.core.response.dto'; import { SettingFileResponseDto } from 'src/modules/setting/dtos/response/setting.file.response.dto'; import { SettingLanguageResponseDto } from 'src/modules/setting/dtos/response/setting.language.response.dto'; import { SettingTimezoneResponseDto } from 'src/modules/setting/dtos/response/setting.timezone.response.dto'; import { SettingService } from 'src/modules/setting/services/setting.service'; +import { SettingSystemCoreDoc } from 'src/modules/setting/docs/setting.system.doc'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; -@ApiTags('modules.private.setting') +@ApiTags('modules.system.setting') @Controller({ version: '1', path: '/setting', }) -export class SettingPrivateController { +export class SettingSystemController { constructor( private readonly messageService: MessageService, private readonly settingService: SettingService ) {} - @SettingPrivateCoreDoc() + @SettingSystemCoreDoc() @Response('setting.core') - @ApiKeyPrivateProtected() + @ApiKeySystemProtected() @Get('/core') async getUserMaxCertificate(): Promise> { const availableLanguage: ENUM_MESSAGE_LANGUAGE[] = diff --git a/src/modules/setting/docs/setting.admin.doc.ts b/src/modules/setting/docs/setting.admin.doc.ts index bfa0d240d..c2ca8f65b 100644 --- a/src/modules/setting/docs/setting.admin.doc.ts +++ b/src/modules/setting/docs/setting.admin.doc.ts @@ -1,6 +1,4 @@ import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { Doc, DocAuth, @@ -11,11 +9,13 @@ import { DocResponse, DocResponsePaging, } from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; import { SettingDocParamsId } from 'src/modules/setting/constants/setting.doc.constant'; -import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/constants/setting.status-code.constant'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/enums/setting.status-code.enum'; import { SettingUpdateRequestDto } from 'src/modules/setting/dtos/request/setting.update.request.dto'; import { SettingGetResponseDto } from 'src/modules/setting/dtos/response/setting.get.response.dto'; import { SettingListResponseDto } from 'src/modules/setting/dtos/response/setting.list.response.dto'; +import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; export function SettingAdminListDoc(): MethodDecorator { return applyDecorators( @@ -44,7 +44,7 @@ export function SettingAdminGetDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'setting.error.notFound', }), ]) @@ -70,13 +70,12 @@ export function SettingAdminUpdateDoc(): MethodDecorator { DocErrorGroup([ DocDefault({ httpStatus: HttpStatus.NOT_FOUND, - statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND, messagePath: 'setting.error.notFound', }), DocDefault({ httpStatus: HttpStatus.BAD_REQUEST, - statusCode: - ENUM_SETTING_STATUS_CODE_ERROR.VALUE_NOT_ALLOWED_ERROR, + statusCode: ENUM_SETTING_STATUS_CODE_ERROR.VALUE_NOT_ALLOWED, messagePath: 'setting.error.valueNotAllowed', }), ]) diff --git a/src/modules/setting/docs/setting.private.doc.ts b/src/modules/setting/docs/setting.system.doc.ts similarity index 89% rename from src/modules/setting/docs/setting.private.doc.ts rename to src/modules/setting/docs/setting.system.doc.ts index b20d85ca9..0ba585480 100644 --- a/src/modules/setting/docs/setting.private.doc.ts +++ b/src/modules/setting/docs/setting.system.doc.ts @@ -3,7 +3,7 @@ import { DocAuth } from 'src/common/doc/decorators/doc.decorator'; import { Doc, DocResponse } from 'src/common/doc/decorators/doc.decorator'; import { SettingCoreResponseDto } from 'src/modules/setting/dtos/response/setting.core.response.dto'; -export function SettingPrivateCoreDoc(): MethodDecorator { +export function SettingSystemCoreDoc(): MethodDecorator { return applyDecorators( Doc({ summary: 'get core' }), DocAuth({ xApiKey: true }), diff --git a/src/modules/setting/dtos/request/setting.create.request.dto.ts b/src/modules/setting/dtos/request/setting.create.request.dto.ts index adfea6aaf..83ec9e6e2 100644 --- a/src/modules/setting/dtos/request/setting.create.request.dto.ts +++ b/src/modules/setting/dtos/request/setting.create.request.dto.ts @@ -2,14 +2,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; import { SafeString } from 'src/common/request/validations/request.safe-string.validation'; -import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/constants/setting.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/enums/setting.enum'; export class SettingCreateRequestDto { @IsString() @IsNotEmpty() @SafeString() @Type(() => String) - readonly name: string; + name: string; @IsString() @IsOptional() @@ -20,7 +20,7 @@ export class SettingCreateRequestDto { description: 'The description about setting', nullable: true, }) - readonly description?: string; + description?: string; @IsString() @IsNotEmpty() @@ -30,7 +30,7 @@ export class SettingCreateRequestDto { required: true, enum: ENUM_SETTING_DATA_TYPE, }) - readonly type: ENUM_SETTING_DATA_TYPE; + type: ENUM_SETTING_DATA_TYPE; @IsNotEmpty() @Type(() => String) @@ -44,5 +44,5 @@ export class SettingCreateRequestDto { { type: 'boolean', readOnly: true, examples: [true, false] }, ], }) - readonly value: string; + value: string; } diff --git a/src/modules/setting/dtos/response/setting.core.response.dto.ts b/src/modules/setting/dtos/response/setting.core.response.dto.ts index 3fcb44a25..86678266c 100644 --- a/src/modules/setting/dtos/response/setting.core.response.dto.ts +++ b/src/modules/setting/dtos/response/setting.core.response.dto.ts @@ -7,7 +7,7 @@ import { SettingTimezoneResponseDto } from 'src/modules/setting/dtos/response/se export class SettingCoreResponseDto { @ApiProperty({ required: true, - type: () => SettingFileResponseDto, + type: SettingFileResponseDto, oneOf: [{ $ref: getSchemaPath(SettingFileResponseDto) }], }) @Type(() => SettingFileResponseDto) @@ -15,7 +15,7 @@ export class SettingCoreResponseDto { @ApiProperty({ required: true, - type: () => SettingLanguageResponseDto, + type: SettingLanguageResponseDto, oneOf: [{ $ref: getSchemaPath(SettingLanguageResponseDto) }], }) @Type(() => SettingLanguageResponseDto) @@ -23,7 +23,7 @@ export class SettingCoreResponseDto { @ApiProperty({ required: true, - type: () => SettingTimezoneResponseDto, + type: SettingTimezoneResponseDto, oneOf: [{ $ref: getSchemaPath(SettingTimezoneResponseDto) }], }) @Type(() => SettingTimezoneResponseDto) diff --git a/src/modules/setting/dtos/response/setting.file.response.dto.ts b/src/modules/setting/dtos/response/setting.file.response.dto.ts index 6af48e710..8dc465738 100644 --- a/src/modules/setting/dtos/response/setting.file.response.dto.ts +++ b/src/modules/setting/dtos/response/setting.file.response.dto.ts @@ -4,5 +4,5 @@ export class SettingFileResponseDto { @ApiProperty({ required: true, }) - readonly sizeInBytes: number; + sizeInBytes: number; } diff --git a/src/modules/setting/dtos/response/setting.get.response.dto.ts b/src/modules/setting/dtos/response/setting.get.response.dto.ts index 00a045673..ca9a053ef 100644 --- a/src/modules/setting/dtos/response/setting.get.response.dto.ts +++ b/src/modules/setting/dtos/response/setting.get.response.dto.ts @@ -1,17 +1,15 @@ -import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude } from 'class-transformer'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/constants/setting.enum.constant'; +import { ApiProperty } from '@nestjs/swagger'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; +import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/enums/setting.enum'; -export class SettingGetResponseDto extends DatabaseIdResponseDto { +export class SettingGetResponseDto extends DatabaseDto { @ApiProperty({ description: 'Name of setting', example: 'MaintenanceOn', required: true, nullable: false, }) - readonly name: string; + name: string; @ApiProperty({ description: 'Description of setting', @@ -19,7 +17,7 @@ export class SettingGetResponseDto extends DatabaseIdResponseDto { required: false, nullable: true, }) - readonly description?: string; + description?: string; @ApiProperty({ description: 'Data type of setting', @@ -28,7 +26,7 @@ export class SettingGetResponseDto extends DatabaseIdResponseDto { nullable: false, enum: ENUM_SETTING_DATA_TYPE, }) - readonly type: ENUM_SETTING_DATA_TYPE; + type: ENUM_SETTING_DATA_TYPE; @ApiProperty({ description: 'Value of string, can be type string/boolean/number', @@ -40,25 +38,5 @@ export class SettingGetResponseDto extends DatabaseIdResponseDto { required: true, nullable: false, }) - readonly value: T; - - @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; + value: T; } diff --git a/src/modules/setting/dtos/response/setting.language.response.dto.ts b/src/modules/setting/dtos/response/setting.language.response.dto.ts index 970828f52..af8cba48d 100644 --- a/src/modules/setting/dtos/response/setting.language.response.dto.ts +++ b/src/modules/setting/dtos/response/setting.language.response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; export class SettingLanguageResponseDto { @ApiProperty({ diff --git a/src/modules/setting/dtos/response/setting.timezone.response.dto.ts b/src/modules/setting/dtos/response/setting.timezone.response.dto.ts index 100514e20..1fb8b372d 100644 --- a/src/modules/setting/dtos/response/setting.timezone.response.dto.ts +++ b/src/modules/setting/dtos/response/setting.timezone.response.dto.ts @@ -4,10 +4,10 @@ export class SettingTimezoneResponseDto { @ApiProperty({ required: true, }) - readonly timezone: string; + timezone: string; @ApiProperty({ required: true, }) - readonly timezoneOffset: string; + timezoneOffset: string; } diff --git a/src/modules/setting/constants/setting.enum.constant.ts b/src/modules/setting/enums/setting.enum.ts similarity index 100% rename from src/modules/setting/constants/setting.enum.constant.ts rename to src/modules/setting/enums/setting.enum.ts diff --git a/src/modules/setting/enums/setting.status-code.enum.ts b/src/modules/setting/enums/setting.status-code.enum.ts new file mode 100644 index 000000000..6436660f5 --- /dev/null +++ b/src/modules/setting/enums/setting.status-code.enum.ts @@ -0,0 +1,4 @@ +export enum ENUM_SETTING_STATUS_CODE_ERROR { + NOT_FOUND = 5130, + VALUE_NOT_ALLOWED = 5131, +} diff --git a/src/modules/setting/interfaces/setting.service.interface.ts b/src/modules/setting/interfaces/setting.service.interface.ts index 5dafe9e0f..d048233e1 100644 --- a/src/modules/setting/interfaces/setting.service.interface.ts +++ b/src/modules/setting/interfaces/setting.service.interface.ts @@ -1,12 +1,12 @@ import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, } from 'src/common/database/interfaces/database.interface'; -import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/constants/setting.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/enums/setting.enum'; import { SettingCreateRequestDto } from 'src/modules/setting/dtos/request/setting.create.request.dto'; import { SettingUpdateRequestDto } from 'src/modules/setting/dtos/request/setting.update.request.dto'; import { SettingGetResponseDto } from 'src/modules/setting/dtos/response/setting.get.response.dto'; @@ -20,15 +20,12 @@ export interface ISettingService { ): Promise; findOne( find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; + findOneById(_id: string, options?: IDatabaseOptions): Promise; findOneByName( name: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; getTotal( find?: Record, @@ -40,16 +37,12 @@ export interface ISettingService { ): Promise; update( repository: SettingDoc, - { value, description }: SettingUpdateRequestDto, - options?: IDatabaseSaveOptions - ): Promise; - delete( - repository: SettingDoc, + { description, value }: SettingUpdateRequestDto, options?: IDatabaseSaveOptions ): Promise; deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise; getValue(type: ENUM_SETTING_DATA_TYPE, value: string): T; checkValue(type: ENUM_SETTING_DATA_TYPE, value: string): boolean; @@ -58,5 +51,5 @@ export interface ISettingService { mapList( settings: SettingDoc[] ): Promise[]>; - mapGet(settings: SettingDoc): Promise>; + mapGet(setting: SettingDoc): Promise>; } diff --git a/src/modules/setting/pipes/setting.parse.pipe.ts b/src/modules/setting/pipes/setting.parse.pipe.ts index 421fa0a89..3d44bbd26 100644 --- a/src/modules/setting/pipes/setting.parse.pipe.ts +++ b/src/modules/setting/pipes/setting.parse.pipe.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; -import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/constants/setting.status-code.constant'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/modules/setting/enums/setting.status-code.enum'; import { SettingDoc } from 'src/modules/setting/repository/entities/setting.entity'; import { SettingService } from 'src/modules/setting/services/setting.service'; @@ -12,7 +12,7 @@ export class SettingParsePipe implements PipeTransform { await this.settingService.findOneById(value); if (!setting) { throw new NotFoundException({ - statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_SETTING_STATUS_CODE_ERROR.NOT_FOUND, message: 'setting.error.notFound', }); } diff --git a/src/modules/setting/repository/entities/setting.entity.ts b/src/modules/setting/repository/entities/setting.entity.ts index 17f53c0d5..cc48f06c3 100644 --- a/src/modules/setting/repository/entities/setting.entity.ts +++ b/src/modules/setting/repository/entities/setting.entity.ts @@ -1,16 +1,16 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; -import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/constants/setting.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/enums/setting.enum'; import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; export const SettingTableName = 'Settings'; @DatabaseEntity({ collection: SettingTableName }) -export class SettingEntity extends DatabaseMongoUUIDEntityAbstract { +export class SettingEntity extends DatabaseEntityAbstract { @DatabaseProp({ required: true, index: true, diff --git a/src/modules/setting/repository/repositories/setting.repository.ts b/src/modules/setting/repository/repositories/setting.repository.ts index 9e08578c8..fa3f3ce91 100644 --- a/src/modules/setting/repository/repositories/setting.repository.ts +++ b/src/modules/setting/repository/repositories/setting.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/database.repository.abstract'; import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; import { SettingDoc, @@ -8,7 +8,7 @@ import { } from 'src/modules/setting/repository/entities/setting.entity'; @Injectable() -export class SettingRepository extends DatabaseMongoUUIDRepositoryAbstract< +export class SettingRepository extends DatabaseRepositoryAbstract< SettingEntity, SettingDoc > { diff --git a/src/modules/setting/services/setting.service.ts b/src/modules/setting/services/setting.service.ts index 4d170cfd1..bf8b5e5ad 100644 --- a/src/modules/setting/services/setting.service.ts +++ b/src/modules/setting/services/setting.service.ts @@ -2,16 +2,16 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, } from 'src/common/database/interfaces/database.interface'; -import { ENUM_HELPER_DATE_FORMAT } from 'src/common/helper/constants/helper.enum.constant'; +import { ENUM_HELPER_DATE_FORMAT } from 'src/common/helper/enums/helper.enum'; import { HelperDateService } from 'src/common/helper/services/helper.date.service'; import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; -import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/constants/setting.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/modules/setting/enums/setting.enum'; import { SettingCreateRequestDto } from 'src/modules/setting/dtos/request/setting.create.request.dto'; import { SettingUpdateRequestDto } from 'src/modules/setting/dtos/request/setting.update.request.dto'; import { SettingGetResponseDto } from 'src/modules/setting/dtos/response/setting.get.response.dto'; @@ -45,28 +45,28 @@ export class SettingService implements ISettingService { find?: Record, options?: IDatabaseFindAllOptions ): Promise { - return this.settingRepository.findAll(find, options); + return this.settingRepository.findAll(find, options); } async findOne( find: Record, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.settingRepository.findOne(find, options); + return this.settingRepository.findOne(find, options); } async findOneById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.settingRepository.findOneById(_id, options); + return this.settingRepository.findOneById(_id, options); } async findOneByName( name: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { - return this.settingRepository.findOne({ name }, options); + return this.settingRepository.findOne({ name }, options); } async getTotal( @@ -82,7 +82,7 @@ export class SettingService implements ISettingService { ): Promise { const create: SettingEntity = new SettingEntity(); create.name = name; - create.description = description ?? undefined; + create.description = description; create.value = value; create.type = type; @@ -100,18 +100,17 @@ export class SettingService implements ISettingService { return this.settingRepository.save(repository, options); } - async delete( - repository: SettingDoc, - options?: IDatabaseSaveOptions - ): Promise { - return this.settingRepository.softDelete(repository, options); - } - async deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise { - return this.settingRepository.deleteMany(find, options); + try { + await this.settingRepository.deleteMany(find, options); + + return true; + } catch (error: unknown) { + throw error; + } } getValue(type: ENUM_SETTING_DATA_TYPE, value: string): T { diff --git a/src/modules/user/constants/user-history.enum.constant.ts b/src/modules/user/constants/user-history.enum.constant.ts deleted file mode 100644 index 8b3a57862..000000000 --- a/src/modules/user/constants/user-history.enum.constant.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ENUM_USER_HISTORY_STATE { - CREATED = 'CREATED', - ACTIVE = 'ACTIVE', - INACTIVE = 'INACTIVE', - DELETED = 'DELETED', - BLOCKED = 'BLOCKED', -} diff --git a/src/modules/user/constants/user.doc.constant.ts b/src/modules/user/constants/user.doc.constant.ts index aba266a33..7bdd40d02 100644 --- a/src/modules/user/constants/user.doc.constant.ts +++ b/src/modules/user/constants/user.doc.constant.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { ENUM_USER_STATUS } from 'src/modules/user/constants/user.enum.constant'; +import { ENUM_USER_STATUS } from 'src/modules/user/enums/user.enum'; export const UserDocParamsId = [ { @@ -21,24 +21,23 @@ export const UserDocQueryRole = [ }, ]; -export const UserDocQueryStatus = [ +export const UserDocQueryCountry = [ { - name: 'status', + name: 'country', allowEmptyValue: true, required: false, type: 'string', - example: Object.values(ENUM_USER_STATUS).join(','), - description: "value with ',' delimiter", + example: faker.string.uuid(), }, ]; -export const UserDocQueryBlocked = [ +export const UserDocQueryStatus = [ { - name: 'blocked', + name: 'status', allowEmptyValue: true, required: false, type: 'string', - example: 'true,false', + example: Object.values(ENUM_USER_STATUS).join(','), description: "value with ',' delimiter", }, ]; diff --git a/src/modules/user/constants/user.list.constant.ts b/src/modules/user/constants/user.list.constant.ts index 328aaea4d..6806fe06b 100644 --- a/src/modules/user/constants/user.list.constant.ts +++ b/src/modules/user/constants/user.list.constant.ts @@ -1,6 +1,4 @@ -import { ENUM_USER_STATUS } from 'src/modules/user/constants/user.enum.constant'; +import { ENUM_USER_STATUS } from 'src/modules/user/enums/user.enum'; -export const USER_DEFAULT_ORDER_BY = 'createdAt'; -export const USER_DEFAULT_AVAILABLE_ORDER_BY = ['createdAt']; +export const USER_DEFAULT_AVAILABLE_SEARCH = ['name', 'email']; export const USER_DEFAULT_STATUS = Object.values(ENUM_USER_STATUS); -export const USER_DEFAULT_BLOCKED = [true, false]; diff --git a/src/modules/user/constants/user.status-code.constant.ts b/src/modules/user/constants/user.status-code.constant.ts deleted file mode 100644 index 71c424c7b..000000000 --- a/src/modules/user/constants/user.status-code.constant.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum ENUM_USER_STATUS_CODE_ERROR { - NOT_FOUND_ERROR = 5150, - EMAIL_EXIST_ERROR = 5151, - MOBILE_NUMBER_EXIST_ERROR = 5152, - STATUS_INVALID_ERROR = 5153, - BLOCKED_INVALID_ERROR = 5154, - FORBIDDEN_INACTIVE_ERROR = 5155, - FORBIDDEN_DELETED_ERROR = 5156, - FORBIDDEN_BLOCKED_ERROR = 5157, - FORBIDDEN_ROLE_INACTIVE_ERROR = 5158, - PASSWORD_NOT_MATCH_ERROR = 5159, - PASSWORD_MUST_NEW_ERROR = 5160, - PASSWORD_EXPIRED_ERROR = 5161, - PASSWORD_ATTEMPT_MAX_ERROR = 5162, -} diff --git a/src/modules/user/controllers/user.admin.controller.ts b/src/modules/user/controllers/user.admin.controller.ts index 29163bb57..a34d64aa0 100644 --- a/src/modules/user/controllers/user.admin.controller.ts +++ b/src/modules/user/controllers/user.admin.controller.ts @@ -20,84 +20,66 @@ import { IResponse, IResponsePaging, } from 'src/common/response/interfaces/response.interface'; -import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; -import { UserService } from 'src/modules/user/services/user.service'; -import { - USER_DEFAULT_AVAILABLE_ORDER_BY, - USER_DEFAULT_BLOCKED, - USER_DEFAULT_ORDER_BY, - USER_DEFAULT_STATUS, -} from 'src/modules/user/constants/user.list.constant'; import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; import { PaginationQuery, PaginationQueryFilterEqual, - PaginationQueryFilterInBoolean, PaginationQueryFilterInEnum, } from 'src/common/pagination/decorators/pagination.decorator'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; import { PolicyAbilityProtected, PolicyRoleProtected, -} from 'src/common/policy/decorators/policy.decorator'; +} from 'src/modules/policy/decorators/policy.decorator'; import { ENUM_POLICY_ACTION, ENUM_POLICY_ROLE_TYPE, ENUM_POLICY_SUBJECT, -} from 'src/common/policy/constants/policy.enum.constant'; +} from 'src/modules/policy/enums/policy.enum'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { AuthJwtAccessProtected } from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/enums/role.status-code.enum'; +import { IAuthPassword } from 'src/modules/auth/interfaces/auth.interface'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { ClientSession, Connection } from 'mongoose'; +import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; +import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/enums/country.status-code.enum'; +import { CountryService } from 'src/modules/country/services/country.service'; import { UserAdminActiveDoc, UserAdminBlockedDoc, UserAdminCreateDoc, UserAdminGetDoc, - UserAdminGetLoginHistoryListDoc, - UserAdminGetPasswordHistoryListDoc, - UserAdminGetStateHistoryListDoc, UserAdminInactiveDoc, UserAdminListDoc, - UserAdminUpdatePasswordDoc, + UserAdminUpdateDoc, } from 'src/modules/user/docs/user.admin.doc'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { - AuthJwtAccessProtected, - AuthJwtPayload, -} from 'src/common/auth/decorators/auth.jwt.decorator'; import { ENUM_USER_SIGN_UP_FROM, ENUM_USER_STATUS, -} from 'src/modules/user/constants/user.enum.constant'; -import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; -import { UserParsePipe } from 'src/modules/user/pipes/user.parse.pipe'; -import { - UserStatusActivePipe, - UserStatusInactivePipe, -} from 'src/modules/user/pipes/user.status.pipe'; -import { UserNotBlockedPipe } from 'src/modules/user/pipes/user.blocked.pipe'; +} from 'src/modules/user/enums/user.enum'; import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; +import { UserParsePipe } from 'src/modules/user/pipes/user.parse.pipe'; import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; +import { UserService } from 'src/modules/user/services/user.service'; +import { + USER_DEFAULT_AVAILABLE_SEARCH, + USER_DEFAULT_STATUS, +} from 'src/modules/user/constants/user.list.constant'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; import { UserNotSelfPipe } from 'src/modules/user/pipes/user.not-self.pipe'; +import { UserStatusPipe } from 'src/modules/user/pipes/user.status.pipe'; +import { UserUpdateRequestDto } from 'src/modules/user/dtos/request/user.update.request.dto'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; -import { EmailService } from 'src/modules/email/services/email.service'; -import { RoleService } from 'src/modules/role/services/role.service'; -import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { AuthService } from 'src/common/auth/services/auth.service'; -import { ClientSession, Connection } from 'mongoose'; -import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/constants/app.status-code.constant'; -import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; -import { CountryService } from 'src/modules/country/services/country.service'; -import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/constants/country.status-code.constant'; -import { UserStateHistoryService } from 'src/modules/user/services/user-state-history.service'; -import { UserPasswordHistoryService } from 'src/modules/user/services/user-password-history.service'; -import { UserStateHistoryDoc } from 'src/modules/user/repository/entities/user-state-history.entity'; -import { UserPasswordHistoryDoc } from 'src/modules/user/repository/entities/user-password-history.entity'; -import { UserStateHistoryListResponseDto } from 'src/modules/user/dtos/response/user-state-history.list.response.dto'; -import { UserPasswordHistoryListResponseDto } from 'src/modules/user/dtos/response/user-password-history.list.response.dto'; -import { UserLoginHistoryDoc } from 'src/modules/user/repository/entities/user-login-history.entity'; -import { UserLoginHistoryListResponseDto } from 'src/modules/user/dtos/response/user-login-history.list.response.dto'; -import { UserLoginHistoryService } from 'src/modules/user/services/user-login-history.service'; +import { ENUM_EMAIL } from 'src/modules/email/enums/email.enum'; +import { Queue } from 'bullmq'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; +import { WorkerQueue } from 'src/worker/decorators/worker.decorator'; @ApiTags('modules.admin.user') @Controller({ @@ -107,14 +89,12 @@ import { UserLoginHistoryService } from 'src/modules/user/services/user-login-hi export class UserAdminController { constructor( @DatabaseConnection() private readonly databaseConnection: Connection, + @WorkerQueue(ENUM_WORKER_QUEUES.EMAIL_QUEUE) + private readonly emailQueue: Queue, private readonly paginationService: PaginationService, private readonly roleService: RoleService, - private readonly emailService: EmailService, private readonly authService: AuthService, private readonly userService: UserService, - private readonly userStateHistoryService: UserStateHistoryService, - private readonly userPasswordHistoryService: UserPasswordHistoryService, - private readonly userLoginHistoryService: UserLoginHistoryService, private readonly countryService: CountryService ) {} @@ -126,12 +106,11 @@ export class UserAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/list') async list( @PaginationQuery({ - defaultOrderBy: USER_DEFAULT_ORDER_BY, - availableOrderBy: USER_DEFAULT_AVAILABLE_ORDER_BY, + availableSearch: USER_DEFAULT_AVAILABLE_SEARCH, }) { _search, _limit, _offset, _order }: PaginationListDto, @PaginationQueryFilterInEnum( @@ -140,16 +119,16 @@ export class UserAdminController { ENUM_USER_STATUS ) status: Record, - @PaginationQueryFilterInBoolean('blocked', USER_DEFAULT_BLOCKED) - blocked: Record, @PaginationQueryFilterEqual('role') - role: Record + role: Record, + @PaginationQueryFilterEqual('country') + country: Record ): Promise> { const find: Record = { ..._search, ...status, - ...blocked, ...role, + ...country, }; const users: IUserDoc[] = @@ -182,7 +161,7 @@ export class UserAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Get('/get/:user') async get( @Param('user', RequestRequiredPipe, UserParsePipe) user: UserDoc @@ -194,146 +173,6 @@ export class UserAdminController { return { data: mapped }; } - @UserAdminGetStateHistoryListDoc() - @ResponsePaging('user.stateHistoryList') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.USER, - action: [ENUM_POLICY_ACTION.READ], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Get('/get/:user/state/history') - async stateHistoryList( - @Param('user', RequestRequiredPipe, UserParsePipe) user: UserDoc, - @PaginationQuery() - { _search, _limit, _offset, _order }: PaginationListDto - ): Promise> { - const find: Record = { - ..._search, - }; - - const userHistories: UserStateHistoryDoc[] = - await this.userStateHistoryService.findAllByUser(user._id, find, { - paging: { - limit: _limit, - offset: _offset, - }, - order: _order, - }); - const total: number = await this.userStateHistoryService.getTotalByUser( - user._id, - find - ); - const totalPage: number = this.paginationService.totalPage( - total, - _limit - ); - - const mapped = - await this.userStateHistoryService.mapList(userHistories); - - return { - _pagination: { total, totalPage }, - data: mapped, - }; - } - - @UserAdminGetPasswordHistoryListDoc() - @ResponsePaging('user.passwordHistoryList') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.USER, - action: [ENUM_POLICY_ACTION.READ], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Get('/get/:user/password/history') - async passwordHistoryList( - @Param('user', RequestRequiredPipe, UserParsePipe) user: UserDoc, - @PaginationQuery() - { _search, _limit, _offset, _order }: PaginationListDto - ): Promise> { - const find: Record = { - ..._search, - }; - - const userHistories: UserPasswordHistoryDoc[] = - await this.userPasswordHistoryService.findAllByUser( - user._id, - find, - { - paging: { - limit: _limit, - offset: _offset, - }, - order: _order, - } - ); - const total: number = - await this.userPasswordHistoryService.getTotalByUser( - user._id, - find - ); - const totalPage: number = this.paginationService.totalPage( - total, - _limit - ); - - const mapped = - await this.userPasswordHistoryService.mapList(userHistories); - - return { - _pagination: { total, totalPage }, - data: mapped, - }; - } - - @UserAdminGetLoginHistoryListDoc() - @ResponsePaging('user.loginHistoryList') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.USER, - action: [ENUM_POLICY_ACTION.READ], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Get('/get/:user/login/history') - async loginHistoryList( - @Param('user', RequestRequiredPipe, UserParsePipe) user: UserDoc, - @PaginationQuery() - { _search, _limit, _offset, _order }: PaginationListDto - ): Promise> { - const find: Record = { - ..._search, - }; - - const userHistories: UserLoginHistoryDoc[] = - await this.userLoginHistoryService.findAllByUser(user._id, find, { - paging: { - limit: _limit, - offset: _offset, - }, - order: _order, - }); - const total: number = await this.userLoginHistoryService.getTotalByUser( - user._id, - find - ); - const totalPage: number = this.paginationService.totalPage( - total, - _limit - ); - - const mapped = - await this.userLoginHistoryService.mapList(userHistories); - - return { - _pagination: { total, totalPage }, - data: mapped, - }; - } - @UserAdminCreateDoc() @Response('user.create') @PolicyAbilityProtected({ @@ -345,8 +184,7 @@ export class UserAdminController { @Post('/create') async create( @Body() - { email, role, name, country }: UserCreateRequestDto, - @AuthJwtPayload('_id') _id: string + { email, role, name, country }: UserCreateRequestDto ): Promise> { const promises: Promise[] = [ this.roleService.findOneById(role), @@ -359,17 +197,17 @@ export class UserAdminController { if (!checkRole) { throw new NotFoundException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND, message: 'role.error.notFound', }); } else if (!checkCountry) { throw new NotFoundException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND, message: 'country.error.notFound', }); } else if (emailExist) { throw new ConflictException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.EMAIL_EXIST_ERROR, + statusCode: ENUM_USER_STATUS_CODE_ERROR.EMAIL_EXIST, message: 'user.error.emailExist', }); } @@ -394,26 +232,22 @@ export class UserAdminController { ENUM_USER_SIGN_UP_FROM.ADMIN, { session } ); - await this.userStateHistoryService.createCreated( - created, - created._id, + + this.emailQueue.add( + ENUM_EMAIL.WELCOME_ADMIN, + { + email: created.email, + name: created.name, + passwordExpiredAt: password.passwordExpired, + password: passwordString, + }, { - session, + debounce: { + id: `${ENUM_EMAIL.WELCOME_ADMIN}-${created._id}`, + ttl: 1000, + }, } ); - await this.userPasswordHistoryService.createByAdmin(created, _id, { - session, - }); - - const emailSend = { - email, - name, - }; - await this.emailService.sendWelcome(emailSend); - await this.emailService.sendTempPassword(emailSend, { - password: passwordString, - expiredAt: password.passwordExpired, - }); await session.commitTransaction(); await session.endSession(); @@ -426,13 +260,54 @@ export class UserAdminController { await session.endSession(); throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, message: 'http.serverError.internalServerError', _error: err.message, }); } } + @UserAdminUpdateDoc() + @Response('user.update') + @PolicyAbilityProtected({ + subject: ENUM_POLICY_SUBJECT.USER, + action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.UPDATE], + }) + @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Put('/update/:user') + async update( + @Param( + 'user', + RequestRequiredPipe, + UserParsePipe, + UserNotSelfPipe, + new UserStatusPipe([ENUM_USER_STATUS.ACTIVE]) + ) + user: UserDoc, + @Body() { name, country, role }: UserUpdateRequestDto + ): Promise { + const checkRole = await this.roleService.findOneActiveById(role); + if (!checkRole) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND, + message: 'role.error.notFound', + }); + } + + const checkCountry = + await this.countryService.findOneActiveById(country); + if (!checkCountry) { + throw new NotFoundException({ + statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'country.error.notFound', + }); + } + + await this.userService.update(user, { name, country, role }); + } + @UserAdminInactiveDoc() @Response('user.inactive') @PolicyAbilityProtected({ @@ -441,7 +316,7 @@ export class UserAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:user/inactive') async inactive( @Param( @@ -449,10 +324,9 @@ export class UserAdminController { RequestRequiredPipe, UserParsePipe, UserNotSelfPipe, - UserStatusActivePipe + new UserStatusPipe([ENUM_USER_STATUS.ACTIVE]) ) - user: UserDoc, - @AuthJwtPayload('_id') _id: string + user: UserDoc ): Promise { const session: ClientSession = await this.databaseConnection.startSession(); @@ -460,9 +334,6 @@ export class UserAdminController { try { await this.userService.inactive(user, { session }); - await this.userStateHistoryService.createInactive(user, _id, { - session, - }); await session.commitTransaction(); await session.endSession(); @@ -473,7 +344,7 @@ export class UserAdminController { await session.endSession(); throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, message: 'http.serverError.internalServerError', _error: err.message, }); @@ -488,7 +359,7 @@ export class UserAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:user/active') async active( @Param( @@ -496,10 +367,9 @@ export class UserAdminController { RequestRequiredPipe, UserParsePipe, UserNotSelfPipe, - UserStatusInactivePipe + new UserStatusPipe([ENUM_USER_STATUS.INACTIVE]) ) - user: UserDoc, - @AuthJwtPayload('_id') _id: string + user: UserDoc ): Promise { const session: ClientSession = await this.databaseConnection.startSession(); @@ -507,9 +377,6 @@ export class UserAdminController { try { await this.userService.active(user, { session }); - await this.userStateHistoryService.createActive(user, _id, { - session, - }); await session.commitTransaction(); await session.endSession(); @@ -520,7 +387,7 @@ export class UserAdminController { await session.endSession(); throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, message: 'http.serverError.internalServerError', _error: err.message, }); @@ -535,7 +402,7 @@ export class UserAdminController { }) @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Patch('/update/:user/blocked') async blocked( @Param( @@ -543,10 +410,12 @@ export class UserAdminController { RequestRequiredPipe, UserParsePipe, UserNotSelfPipe, - UserNotBlockedPipe + new UserStatusPipe([ + ENUM_USER_STATUS.INACTIVE, + ENUM_USER_STATUS.ACTIVE, + ]) ) - user: UserDoc, - @AuthJwtPayload('_id') _id: string + user: UserDoc ): Promise { const session: ClientSession = await this.databaseConnection.startSession(); @@ -554,68 +423,6 @@ export class UserAdminController { try { await this.userService.blocked(user, { session }); - await this.userStateHistoryService.createBlocked(user, _id, { - session, - }); - - await session.commitTransaction(); - await session.endSession(); - - return; - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - - throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, - message: 'http.serverError.internalServerError', - _error: err.message, - }); - } - } - - @UserAdminUpdatePasswordDoc() - @Response('user.updatePassword') - @PolicyAbilityProtected({ - subject: ENUM_POLICY_SUBJECT.USER, - action: [ENUM_POLICY_ACTION.READ, ENUM_POLICY_ACTION.UPDATE], - }) - @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN) - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Put('/update/:user/password') - async updatePassword( - @Param('user', RequestRequiredPipe, UserParsePipe, UserNotSelfPipe) - user: UserDoc, - @AuthJwtPayload('_id') _id: string - ): Promise { - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - const passwordString = - await this.authService.createPasswordRandom(); - const password = - await this.authService.createPassword(passwordString); - user = await this.userService.updatePassword(user, password, { - session, - }); - user = await this.userService.resetPasswordAttempt(user, { - session, - }); - await this.userPasswordHistoryService.createByAdmin(user, _id, { - session, - }); - - const emailSend = { - email: user.email, - name: user.name, - }; - await this.emailService.sendTempPassword(emailSend, { - password: passwordString, - expiredAt: password.passwordExpired, - }); await session.commitTransaction(); await session.endSession(); @@ -626,7 +433,7 @@ export class UserAdminController { await session.endSession(); throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, message: 'http.serverError.internalServerError', _error: err.message, }); diff --git a/src/modules/user/controllers/user.auth.controller.ts b/src/modules/user/controllers/user.auth.controller.ts deleted file mode 100644 index fb916c46e..000000000 --- a/src/modules/user/controllers/user.auth.controller.ts +++ /dev/null @@ -1,640 +0,0 @@ -import { - BadRequestException, - Body, - Controller, - ForbiddenException, - Get, - HttpCode, - HttpStatus, - InternalServerErrorException, - NotFoundException, - Patch, - Post, - Put, - UploadedFile, - Req, -} from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { Request } from 'express'; -import { ClientSession, Connection } from 'mongoose'; -import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/constants/app.status-code.constant'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { ENUM_AUTH_LOGIN_FROM } from 'src/common/auth/constants/auth.enum.constant'; -import { - AuthJwtAccessProtected, - AuthJwtPayload, - AuthJwtRefreshProtected, - AuthJwtToken, -} from 'src/common/auth/decorators/auth.jwt.decorator'; -import { - AuthSocialAppleProtected, - AuthSocialGoogleProtected, -} from 'src/common/auth/decorators/auth.social.decorator'; -import { AuthJwtAccessPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.access-payload.dto'; -import { AuthJwtRefreshPayloadDto } from 'src/common/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; -import { AuthSocialApplePayloadDto } from 'src/common/auth/dtos/social/auth.social.apple-payload.dto'; -import { AuthSocialGooglePayloadDto } from 'src/common/auth/dtos/social/auth.social.google-payload.dto'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { AuthService } from 'src/common/auth/services/auth.service'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; -import { IAwsS3RandomFilename } from 'src/common/aws/interfaces/aws.interface'; -import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; -import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; -import { ENUM_FILE_MIME_IMAGE } from 'src/common/file/constants/file.enum.constant'; -import { FileUploadSingle } from 'src/common/file/decorators/file.decorator'; -import { IFile } from 'src/common/file/interfaces/file.interface'; -import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; -import { FileTypePipe } from 'src/common/file/pipes/file.type.pipe'; -import { Response } from 'src/common/response/decorators/response.decorator'; -import { IResponse } from 'src/common/response/interfaces/response.interface'; -import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/constants/country.status-code.constant'; -import { CountryService } from 'src/modules/country/services/country.service'; -import { ENUM_USER_STATUS } from 'src/modules/user/constants/user.enum.constant'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; -import { - User, - UserProtected, -} from 'src/modules/user/decorators/user.decorator'; -import { - UserAuthChangePasswordDoc, - UserAuthLoginCredentialDoc, - UserAuthLoginSocialAppleDoc, - UserAuthLoginSocialGoogleDoc, - UserAuthProfileDoc, - UserAuthRefreshDoc, - UserAuthUpdateProfileDoc, - UserAuthUploadProfileDoc, -} from 'src/modules/user/docs/user.auth.doc'; -import { UserChangePasswordRequestDto } from 'src/modules/user/dtos/request/user.change-password.request.dto'; -import { UserLoginRequestDto } from 'src/modules/user/dtos/request/user.login.request.dto'; -import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.request.dto'; -import { UserLoginResponseDto } from 'src/modules/user/dtos/response/user.login.response.dto'; -import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; -import { UserRefreshResponseDto } from 'src/modules/user/dtos/response/user.refresh.response.dto'; -import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; -import { UserLoginHistoryService } from 'src/modules/user/services/user-login-history.service'; -import { UserPasswordHistoryService } from 'src/modules/user/services/user-password-history.service'; -import { UserService } from 'src/modules/user/services/user.service'; - -@ApiTags('modules.auth.user') -@Controller({ - version: '1', - path: '/user', -}) -export class UserAuthController { - constructor( - @DatabaseConnection() private readonly databaseConnection: Connection, - private readonly userService: UserService, - private readonly awsS3Service: AwsS3Service, - private readonly authService: AuthService, - private readonly userPasswordHistoryService: UserPasswordHistoryService, - private readonly countryService: CountryService, - private readonly userLoginHistoryService: UserLoginHistoryService - ) {} - - @UserAuthLoginCredentialDoc() - @Response('user.loginWithCredential') - @ApiKeyPublicProtected() - @HttpCode(HttpStatus.OK) - @Post('/login/credential') - async loginWithCredential( - @Body() { email, password }: UserLoginRequestDto, - @Req() request: Request - ): Promise> { - const user: UserDoc = await this.userService.findOneByEmail(email); - if (!user) { - throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'user.error.notFound', - }); - } - - const passwordAttempt: boolean = - await this.authService.getPasswordAttempt(); - const passwordMaxAttempt: number = - await this.authService.getPasswordMaxAttempt(); - if (passwordAttempt && user.passwordAttempt >= passwordMaxAttempt) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_ATTEMPT_MAX_ERROR, - message: 'user.error.passwordAttemptMax', - }); - } - - const validate: boolean = await this.authService.validateUser( - password, - user.password - ); - if (!validate) { - await this.userService.increasePasswordAttempt(user); - - throw new BadRequestException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_NOT_MATCH_ERROR, - message: 'user.error.passwordNotMatch', - }); - } else if (user.blocked) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_BLOCKED_ERROR, - message: 'user.error.blocked', - }); - } else if (user.status === ENUM_USER_STATUS.DELETED) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_DELETED_ERROR, - message: 'user.error.deleted', - }); - } else if (user.status === ENUM_USER_STATUS.INACTIVE) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_INACTIVE_ERROR, - message: 'user.error.inactive', - }); - } - - const userWithRole: IUserDoc = await this.userService.join(user); - if (!userWithRole.role.isActive) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_ROLE_INACTIVE_ERROR, - message: 'user.error.roleInactive', - }); - } - - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - await this.userService.resetPasswordAttempt(user, { session }); - - const checkPasswordExpired: boolean = - await this.authService.checkPasswordExpired( - user.passwordExpired - ); - if (checkPasswordExpired) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED_ERROR, - message: 'user.error.passwordExpired', - }); - } - - await this.userLoginHistoryService.create( - request, - { - user: user._id, - }, - { session } - ); - - await session.commitTransaction(); - await session.endSession(); - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - - throw err; - } - - const roleType = userWithRole.role.type; - const tokenType: string = await this.authService.getTokenType(); - - const expiresInAccessToken: number = - await this.authService.getAccessTokenExpirationTime(); - const payloadAccessToken: AuthJwtAccessPayloadDto = - await this.authService.createPayloadAccessToken( - userWithRole, - ENUM_AUTH_LOGIN_FROM.CREDENTIAL - ); - const accessToken: string = - await this.authService.createAccessToken(payloadAccessToken); - - const payloadRefreshToken: AuthJwtRefreshPayloadDto = - await this.authService.createPayloadRefreshToken( - payloadAccessToken - ); - const refreshToken: string = - await this.authService.createRefreshToken(payloadRefreshToken); - - return { - data: { - tokenType, - roleType, - expiresIn: expiresInAccessToken, - accessToken, - refreshToken, - }, - }; - } - - @UserAuthLoginSocialGoogleDoc() - @Response('user.loginWithSocialGoogle') - @AuthSocialGoogleProtected() - @Post('/login/social/google') - async loginWithGoogle( - @AuthJwtPayload() - { email }: AuthSocialGooglePayloadDto, - @Req() request: Request - ): Promise> { - const user: UserDoc = await this.userService.findOneByEmail(email); - if (!user) { - throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'user.error.notFound', - }); - } else if (user.blocked) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_BLOCKED_ERROR, - message: 'user.error.blocked', - }); - } else if (user.status === ENUM_USER_STATUS.DELETED) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_DELETED_ERROR, - message: 'user.error.deleted', - }); - } else if (user.status === ENUM_USER_STATUS.INACTIVE) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_INACTIVE_ERROR, - message: 'user.error.inactive', - }); - } - - const userWithRole: IUserDoc = await this.userService.join(user); - if (!userWithRole.role.isActive) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_ROLE_INACTIVE_ERROR, - message: 'user.error.roleInactive', - }); - } - - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - await this.userService.resetPasswordAttempt(user, { session }); - - const checkPasswordExpired: boolean = - await this.authService.checkPasswordExpired( - user.passwordExpired - ); - if (checkPasswordExpired) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED_ERROR, - message: 'user.error.passwordExpired', - }); - } - - await this.userLoginHistoryService.create( - request, - { - user: user._id, - }, - { session } - ); - - await session.commitTransaction(); - await session.endSession(); - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - - throw err; - } - - const roleType = userWithRole.role.type; - const tokenType: string = await this.authService.getTokenType(); - - const expiresInAccessToken: number = - await this.authService.getAccessTokenExpirationTime(); - const payloadAccessToken: AuthJwtAccessPayloadDto = - await this.authService.createPayloadAccessToken( - userWithRole, - ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE - ); - const accessToken: string = - await this.authService.createAccessToken(payloadAccessToken); - - const payloadRefreshToken: AuthJwtRefreshPayloadDto = - await this.authService.createPayloadRefreshToken( - payloadAccessToken - ); - const refreshToken: string = - await this.authService.createRefreshToken(payloadRefreshToken); - - return { - data: { - tokenType, - roleType, - expiresIn: expiresInAccessToken, - accessToken, - refreshToken, - }, - }; - } - - @UserAuthLoginSocialAppleDoc() - @Response('user.loginWithSocialApple') - @AuthSocialAppleProtected() - @Post('/login/social/apple') - async loginWithApple( - @AuthJwtPayload() - { email }: AuthSocialApplePayloadDto, - @Req() request: Request - ): Promise> { - const user: UserDoc = await this.userService.findOneByEmail(email); - if (!user) { - throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'user.error.notFound', - }); - } else if (user.blocked) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_BLOCKED_ERROR, - message: 'user.error.blocked', - }); - } else if (user.status === ENUM_USER_STATUS.DELETED) { - throw new ForbiddenException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_DELETED_ERROR, - message: 'user.error.deleted', - }); - } else if (user.status === ENUM_USER_STATUS.INACTIVE) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_INACTIVE_ERROR, - message: 'user.error.inactive', - }); - } - - const userWithRole: IUserDoc = await this.userService.join(user); - if (!userWithRole.role.isActive) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.FORBIDDEN_ROLE_INACTIVE_ERROR, - message: 'user.error.roleInactive', - }); - } - - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - await this.userService.resetPasswordAttempt(user, { session }); - - const checkPasswordExpired: boolean = - await this.authService.checkPasswordExpired( - user.passwordExpired - ); - if (checkPasswordExpired) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_EXPIRED_ERROR, - message: 'user.error.passwordExpired', - }); - } - - await this.userLoginHistoryService.create( - request, - { - user: user._id, - }, - { session } - ); - - await session.commitTransaction(); - await session.endSession(); - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - throw err; - } - - const roleType = userWithRole.role.type; - const tokenType: string = await this.authService.getTokenType(); - - const expiresInAccessToken: number = - await this.authService.getAccessTokenExpirationTime(); - const payloadAccessToken: AuthJwtAccessPayloadDto = - await this.authService.createPayloadAccessToken( - userWithRole, - ENUM_AUTH_LOGIN_FROM.SOCIAL_APPLE - ); - const accessToken: string = - await this.authService.createAccessToken(payloadAccessToken); - - const payloadRefreshToken: AuthJwtRefreshPayloadDto = - await this.authService.createPayloadRefreshToken( - payloadAccessToken - ); - const refreshToken: string = - await this.authService.createRefreshToken(payloadRefreshToken); - - return { - data: { - tokenType, - roleType, - expiresIn: expiresInAccessToken, - accessToken, - refreshToken, - }, - }; - } - - @UserAuthRefreshDoc() - @Response('user.refresh') - @UserProtected() - @AuthJwtRefreshProtected() - @ApiKeyPublicProtected() - @HttpCode(HttpStatus.OK) - @Post('/refresh') - async refresh( - @AuthJwtToken() refreshToken: string, - @AuthJwtPayload() - { loginFrom }: AuthJwtRefreshPayloadDto, - @User(true) user: IUserDoc - ): Promise> { - const roleType = user.role.type; - const tokenType: string = await this.authService.getTokenType(); - - const expiresInAccessToken: number = - await this.authService.getAccessTokenExpirationTime(); - const payloadAccessToken: AuthJwtAccessPayloadDto = - await this.authService.createPayloadAccessToken(user, loginFrom); - const accessToken: string = - await this.authService.createAccessToken(payloadAccessToken); - - return { - data: { - tokenType, - roleType, - expiresIn: expiresInAccessToken, - accessToken, - refreshToken, - }, - }; - } - - @UserAuthChangePasswordDoc() - @Response('user.changePassword') - @UserProtected() - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Patch('/change-password') - async changePassword( - @Body() body: UserChangePasswordRequestDto, - @User() user: UserDoc - ): Promise { - const passwordAttempt: boolean = - await this.authService.getPasswordAttempt(); - const passwordMaxAttempt: number = - await this.authService.getPasswordMaxAttempt(); - if (passwordAttempt && user.passwordAttempt >= passwordMaxAttempt) { - throw new ForbiddenException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_ATTEMPT_MAX_ERROR, - message: 'user.error.passwordAttemptMax', - }); - } - - const matchPassword: boolean = await this.authService.validateUser( - body.oldPassword, - user.password - ); - if (!matchPassword) { - await this.userService.increasePasswordAttempt(user); - - throw new BadRequestException({ - statusCode: - ENUM_USER_STATUS_CODE_ERROR.PASSWORD_NOT_MATCH_ERROR, - message: 'user.error.passwordNotMatch', - }); - } - - const password: IAuthPassword = await this.authService.createPassword( - body.newPassword - ); - const checkUserPassword = - await this.userPasswordHistoryService.checkPasswordPeriodByUser( - user, - password - ); - if (checkUserPassword) { - const passwordPeriod = - await this.userPasswordHistoryService.getPasswordPeriod(); - throw new BadRequestException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.PASSWORD_MUST_NEW_ERROR, - message: 'user.error.passwordMustNew', - _metadata: { - customProperty: { - period: passwordPeriod, - }, - }, - }); - } - - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - user = await this.userService.resetPasswordAttempt(user, { - session, - }); - user = await this.userService.updatePassword(user, password, { - session, - }); - await this.userPasswordHistoryService.createByUser(user, { - session, - }); - - await session.commitTransaction(); - await session.endSession(); - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - - throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, - message: 'http.serverError.internalServerError', - _error: err.message, - }); - } - } - - @UserAuthProfileDoc() - @Response('user.profile') - @UserProtected() - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Get('/profile') - async profile( - @User(true) user: IUserDoc - ): Promise> { - const mapped: UserProfileResponseDto = - await this.userService.mapProfile(user); - return { data: mapped }; - } - - @UserAuthUpdateProfileDoc() - @Response('user.updateProfile') - @UserProtected() - @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Put('/profile/update') - async updateProfile( - @User() user: UserDoc, - @Body() - { country, ...body }: UserUpdateProfileRequestDto - ): Promise { - const checkCountry = this.countryService.findOneActiveById(country); - if (!checkCountry) { - throw new NotFoundException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'country.error.notFound', - }); - } - - await this.userService.updateProfile(user, { country, ...body }); - - return; - } - - @UserAuthUploadProfileDoc() - @Response('user.updateProfileUpload') - @UserProtected() - @AuthJwtAccessProtected() - @FileUploadSingle() - @ApiKeyPublicProtected() - @Post('/profile/upload') - async updateProfileUpload( - @User() user: UserDoc, - @UploadedFile( - new FileRequiredPipe(), - new FileTypePipe([ - ENUM_FILE_MIME_IMAGE.JPG, - ENUM_FILE_MIME_IMAGE.JPEG, - ENUM_FILE_MIME_IMAGE.PNG, - ]) - ) - file: IFile - ): Promise { - const pathPrefix: string = await this.userService.getPhotoUploadPath( - user._id - ); - const randomFilename: IAwsS3RandomFilename = - await this.awsS3Service.createRandomFilename(pathPrefix); - - const aws: AwsS3Dto = await this.awsS3Service.putItemInBucket( - file, - randomFilename - ); - await this.userService.updatePhoto(user, aws); - - return; - } -} diff --git a/src/modules/user/controllers/user.public.controller.ts b/src/modules/user/controllers/user.public.controller.ts deleted file mode 100644 index f939c858c..000000000 --- a/src/modules/user/controllers/user.public.controller.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - Body, - ConflictException, - Controller, - InternalServerErrorException, - NotFoundException, - Post, -} from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { ClientSession, Connection } from 'mongoose'; -import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/constants/app.status-code.constant'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; -import { AuthService } from 'src/common/auth/services/auth.service'; -import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; -import { Response } from 'src/common/response/decorators/response.decorator'; -import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/constants/country.status-code.constant'; -import { CountryService } from 'src/modules/country/services/country.service'; -import { EmailService } from 'src/modules/email/services/email.service'; -import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; -import { RoleService } from 'src/modules/role/services/role.service'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; -import { UserPublicSignUpDoc } from 'src/modules/user/docs/user.public.doc'; -import { UserSignUpRequestDto } from 'src/modules/user/dtos/request/user.sign-up.request.dto'; -import { UserPasswordHistoryService } from 'src/modules/user/services/user-password-history.service'; -import { UserStateHistoryService } from 'src/modules/user/services/user-state-history.service'; -import { UserService } from 'src/modules/user/services/user.service'; - -@ApiTags('modules.public.user') -@Controller({ - version: '1', - path: '/user', -}) -export class UserPublicController { - constructor( - @DatabaseConnection() private readonly databaseConnection: Connection, - private readonly userService: UserService, - private readonly userStateHistoryService: UserStateHistoryService, - private readonly userPasswordHistoryService: UserPasswordHistoryService, - private readonly authService: AuthService, - private readonly roleService: RoleService, - private readonly emailService: EmailService, - private readonly countryService: CountryService - ) {} - - @UserPublicSignUpDoc() - @Response('user.signUp') - @ApiKeyPublicProtected() - @Post('/sign-up') - async signUp( - @Body() - { email, name, password: passwordString, country }: UserSignUpRequestDto - ): Promise { - const promises: Promise[] = [ - this.roleService.findOneByName('user'), - this.userService.existByEmail(email), - this.countryService.findOneActiveById(country), - ]; - - const [role, emailExist, checkCountry] = await Promise.all(promises); - - if (!role) { - throw new NotFoundException({ - statusCode: ENUM_ROLE_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'role.error.notFound', - }); - } else if (!checkCountry) { - throw new NotFoundException({ - statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'country.error.notFound', - }); - } else if (emailExist) { - throw new ConflictException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.EMAIL_EXIST_ERROR, - message: 'user.error.emailExist', - }); - } - - const password = await this.authService.createPassword(passwordString); - - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - const user = await this.userService.signUp( - role._id, - { - email, - name, - password: passwordString, - country, - }, - password, - { session } - ); - await this.userStateHistoryService.createCreated(user, user._id, { - session, - }); - await this.userPasswordHistoryService.createByUser(user, { - session, - }); - - await this.emailService.sendWelcome({ - email, - name, - }); - - await session.commitTransaction(); - await session.endSession(); - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); - - throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, - message: 'http.serverError.internalServerError', - _error: err.message, - }); - } - - return; - } -} diff --git a/src/modules/user/controllers/user.shared.controller.ts b/src/modules/user/controllers/user.shared.controller.ts new file mode 100644 index 000000000..0fc3e7741 --- /dev/null +++ b/src/modules/user/controllers/user.shared.controller.ts @@ -0,0 +1,126 @@ +import { + Body, + Controller, + Get, + NotFoundException, + Post, + Put, + UploadedFile, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FileUploadSingle } from 'src/common/file/decorators/file.decorator'; +import { ENUM_FILE_MIME_IMAGE } from 'src/common/file/enums/file.enum'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; +import { FileTypePipe } from 'src/common/file/pipes/file.type.pipe'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, +} from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; +import { AwsS3Service } from 'src/modules/aws/services/aws.s3.service'; +import { ENUM_COUNTRY_STATUS_CODE_ERROR } from 'src/modules/country/enums/country.status-code.enum'; +import { CountryService } from 'src/modules/country/services/country.service'; +import { + UserSharedProfileDoc, + UserSharedUpdateProfileDoc, + UserSharedUploadProfileDoc, +} from 'src/modules/user/docs/user.shared.doc'; +import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.dto'; +import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { + UserActiveParsePipe, + UserParsePipe, +} from 'src/modules/user/pipes/user.parse.pipe'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@ApiTags('modules.shared.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserSharedController { + constructor( + private readonly awsS3Service: AwsS3Service, + private readonly userService: UserService, + private readonly countryService: CountryService + ) {} + + @UserSharedProfileDoc() + @Response('user.profile') + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Get('/profile') + async profile( + @AuthJwtPayload('_id', UserActiveParsePipe) + user: IUserDoc + ): Promise> { + const mapped: UserProfileResponseDto = + await this.userService.mapProfile(user); + return { data: mapped }; + } + + @UserSharedUpdateProfileDoc() + @Response('user.updateProfile') + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Put('/profile/update') + async updateProfile( + @AuthJwtPayload('_id', UserParsePipe) + user: UserDoc, + @Body() + { country, ...body }: UserUpdateProfileRequestDto + ): Promise { + const checkCountry = this.countryService.findOneActiveById(country); + if (!checkCountry) { + throw new NotFoundException({ + statusCode: ENUM_COUNTRY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'country.error.notFound', + }); + } + + await this.userService.updateProfile(user, { country, ...body }); + + return; + } + + @UserSharedUploadProfileDoc() + @Response('user.updateProfileUpload') + @AuthJwtAccessProtected() + @FileUploadSingle() + @ApiKeyProtected() + @Post('/profile/upload') + async updateProfileUpload( + @AuthJwtPayload('_id', UserParsePipe) + user: UserDoc, + @UploadedFile( + new FileRequiredPipe(), + new FileTypePipe([ + ENUM_FILE_MIME_IMAGE.JPG, + ENUM_FILE_MIME_IMAGE.JPEG, + ENUM_FILE_MIME_IMAGE.PNG, + ]) + ) + file: IFile + ): Promise { + const path: string = await this.userService.getPhotoUploadPath( + user._id + ); + const randomFilename: string = + await this.userService.createRandomFilenamePhoto(); + + const aws: AwsS3Dto = await this.awsS3Service.putItemInBucket(file, { + customFilename: randomFilename, + path, + }); + await this.userService.updatePhoto(user, aws); + + return; + } +} diff --git a/src/modules/user/controllers/user.user.controller.ts b/src/modules/user/controllers/user.user.controller.ts index 61fa87ad5..dc26060a1 100644 --- a/src/modules/user/controllers/user.user.controller.ts +++ b/src/modules/user/controllers/user.user.controller.ts @@ -1,35 +1,30 @@ import { Body, + ConflictException, Controller, Delete, - InternalServerErrorException, Put, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ClientSession } from 'mongoose'; -import { Connection } from 'mongoose'; -import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/constants/app.status-code.constant'; -import { ApiKeyPublicProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { ApiKeyProtected } from 'src/modules/api-key/decorators/api-key.decorator'; import { AuthJwtAccessProtected, AuthJwtPayload, -} from 'src/common/auth/decorators/auth.jwt.decorator'; -import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; -import { ENUM_POLICY_ROLE_TYPE } from 'src/common/policy/constants/policy.enum.constant'; -import { PolicyRoleProtected } from 'src/common/policy/decorators/policy.decorator'; +} from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; +import { PolicyRoleProtected } from 'src/modules/policy/decorators/policy.decorator'; import { Response } from 'src/common/response/decorators/response.decorator'; +import { UserService } from 'src/modules/user/services/user.service'; import { - User, - UserProtected, -} from 'src/modules/user/decorators/user.decorator'; -import { - UserAuthUpdateMobileNumberDoc, - UserUserDeleteSelfDoc, + UserUserDeleteDoc, + UserUserUpdateMobileNumberDoc, + UserUserUpdateUsernameDoc, } from 'src/modules/user/docs/user.user.doc'; import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; +import { UserUpdateClaimUsernameRequestDto } from 'src/modules/user/dtos/request/user.update-claim-username.dto'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; +import { UserParsePipe } from 'src/modules/user/pipes/user.parse.pipe'; import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; -import { UserStateHistoryService } from 'src/modules/user/services/user-state-history.service'; -import { UserService } from 'src/modules/user/services/user.service'; @ApiTags('modules.user.user') @Controller({ @@ -37,20 +32,30 @@ import { UserService } from 'src/modules/user/services/user.service'; path: '/user', }) export class UserUserController { - constructor( - @DatabaseConnection() private readonly databaseConnection: Connection, - private readonly userService: UserService, - private readonly userStateHistoryService: UserStateHistoryService - ) {} + constructor(private readonly userService: UserService) {} + + @UserUserDeleteDoc() + @Response('user.delete') + @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.USER) + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Delete('/delete') + async delete( + @AuthJwtPayload('_id', UserParsePipe) user: UserDoc + ): Promise { + await this.userService.delete(user, { deletedBy: user._id }); + + return; + } - @UserAuthUpdateMobileNumberDoc() + @UserUserUpdateMobileNumberDoc() @Response('user.updateMobileNumber') - @UserProtected() + @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.USER) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() + @ApiKeyProtected() @Put('/update/mobile-number') async updateMobileNumber( - @User() user: UserDoc, + @AuthJwtPayload('_id', UserParsePipe) user: UserDoc, @Body() body: UserUpdateMobileNumberRequestDto ): Promise { @@ -59,42 +64,27 @@ export class UserUserController { return; } - @UserUserDeleteSelfDoc() - @Response('user.deleteSelf') - @UserProtected() + @UserUserUpdateUsernameDoc() + @Response('user.updateClaimUsername') @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.USER) @AuthJwtAccessProtected() - @ApiKeyPublicProtected() - @Delete('/delete') - async deleteSelf( - @User() user: UserDoc, - @AuthJwtPayload('_id') _id: string + @ApiKeyProtected() + @Put('/update/claim-username') + async updateUsername( + @AuthJwtPayload('_id', UserParsePipe) user: UserDoc, + @Body() + { username }: UserUpdateClaimUsernameRequestDto ): Promise { - const session: ClientSession = - await this.databaseConnection.startSession(); - session.startTransaction(); - - try { - await this.userService.selfDelete(user, { - session, - }); - await this.userStateHistoryService.createBlocked(user, _id, { - session, + const checkUsername = await this.userService.existByUsername(username); + if (checkUsername) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USERNAME_EXIST, + message: 'user.error.usernameExist', }); + } - await session.commitTransaction(); - await session.endSession(); - - return; - } catch (err: any) { - await session.abortTransaction(); - await session.endSession(); + await this.userService.updateClaimUsername(user, { username }); - throw new InternalServerErrorException({ - statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN_ERROR, - message: 'http.serverError.internalServerError', - _error: err.message, - }); - } + return; } } diff --git a/src/modules/user/decorators/user.decorator.ts b/src/modules/user/decorators/user.decorator.ts deleted file mode 100644 index 5e874f9b2..000000000 --- a/src/modules/user/decorators/user.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - ExecutionContext, - UseGuards, - applyDecorators, - createParamDecorator, -} from '@nestjs/common'; -import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { UserGuard } from 'src/modules/user/guards/user.guard'; - -export function UserProtected(): MethodDecorator { - return applyDecorators(UseGuards(UserGuard)); -} - -export const User = createParamDecorator((_, ctx: ExecutionContext): T => { - const { __user } = ctx - .switchToHttp() - .getRequest(); - return __user; -}); diff --git a/src/modules/user/docs/user.admin.doc.ts b/src/modules/user/docs/user.admin.doc.ts index 2e9e7f831..c2717b107 100644 --- a/src/modules/user/docs/user.admin.doc.ts +++ b/src/modules/user/docs/user.admin.doc.ts @@ -1,6 +1,5 @@ import { HttpStatus, applyDecorators } from '@nestjs/common'; import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { Doc, DocAuth, @@ -9,16 +8,15 @@ import { DocResponse, DocResponsePaging, } from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; import { UserDocParamsId, - UserDocQueryBlocked, + UserDocQueryCountry, UserDocQueryRole, UserDocQueryStatus, } from 'src/modules/user/constants/user.doc.constant'; import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; -import { UserLoginHistoryListResponseDto } from 'src/modules/user/dtos/response/user-login-history.list.response.dto'; -import { UserPasswordHistoryListResponseDto } from 'src/modules/user/dtos/response/user-password-history.list.response.dto'; -import { UserStateHistoryListResponseDto } from 'src/modules/user/dtos/response/user-state-history.list.response.dto'; +import { UserUpdateRequestDto } from 'src/modules/user/dtos/request/user.update.request.dto'; import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; @@ -30,8 +28,8 @@ export function UserAdminListDoc(): MethodDecorator { DocRequest({ queries: [ ...UserDocQueryStatus, - ...UserDocQueryBlocked, ...UserDocQueryRole, + ...UserDocQueryCountry, ], }), DocAuth({ @@ -45,72 +43,6 @@ export function UserAdminListDoc(): MethodDecorator { ); } -export function UserAdminGetStateHistoryListDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'get all user state histories', - }), - DocRequest({ - params: UserDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponsePaging( - 'user.stateHistoryList', - { - dto: UserStateHistoryListResponseDto, - } - ) - ); -} - -export function UserAdminGetPasswordHistoryListDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'get all user history change password', - }), - DocRequest({ - params: UserDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponsePaging( - 'user.passwordHistoryList', - { - dto: UserPasswordHistoryListResponseDto, - } - ) - ); -} - -export function UserAdminGetLoginHistoryListDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'get all user login history', - }), - DocRequest({ - params: UserDocParamsId, - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocGuard({ role: true, policy: true }), - DocResponsePaging( - 'user.loginHistoryList', - { - dto: UserLoginHistoryListResponseDto, - } - ) - ); -} - export function UserAdminGetDoc(): MethodDecorator { return applyDecorators( Doc({ @@ -168,27 +100,29 @@ export function UserAdminActiveDoc(): MethodDecorator { ); } -export function UserAdminInactiveDoc(): MethodDecorator { +export function UserAdminUpdateDoc(): MethodDecorator { return applyDecorators( Doc({ - summary: 'make user be inactive', + summary: 'update a user', }), DocRequest({ params: UserDocParamsId, + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: UserUpdateRequestDto, }), DocAuth({ xApiKey: true, jwtAccessToken: true, }), DocGuard({ role: true, policy: true }), - DocResponse('user.inactive') + DocResponse('user.update') ); } -export function UserAdminBlockedDoc(): MethodDecorator { +export function UserAdminInactiveDoc(): MethodDecorator { return applyDecorators( Doc({ - summary: 'block a user', + summary: 'make user be inactive', }), DocRequest({ params: UserDocParamsId, @@ -198,14 +132,14 @@ export function UserAdminBlockedDoc(): MethodDecorator { jwtAccessToken: true, }), DocGuard({ role: true, policy: true }), - DocResponse('user.blocked') + DocResponse('user.inactive') ); } -export function UserAdminUpdatePasswordDoc(): MethodDecorator { +export function UserAdminBlockedDoc(): MethodDecorator { return applyDecorators( Doc({ - summary: 'update user password', + summary: 'block a user', }), DocRequest({ params: UserDocParamsId, @@ -215,6 +149,6 @@ export function UserAdminUpdatePasswordDoc(): MethodDecorator { jwtAccessToken: true, }), DocGuard({ role: true, policy: true }), - DocResponse('user.updatePassword') + DocResponse('user.blocked') ); } diff --git a/src/modules/user/docs/user.auth.doc.ts b/src/modules/user/docs/user.auth.doc.ts deleted file mode 100644 index e3a25b6bc..000000000 --- a/src/modules/user/docs/user.auth.doc.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; -import { - Doc, - DocAuth, - DocRequest, - DocRequestFile, - DocResponse, -} from 'src/common/doc/decorators/doc.decorator'; -import { FileSingleDto } from 'src/common/file/dtos/file.single.dto'; -import { UserChangePasswordRequestDto } from 'src/modules/user/dtos/request/user.change-password.request.dto'; -import { UserLoginRequestDto } from 'src/modules/user/dtos/request/user.login.request.dto'; -import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.request.dto'; -import { UserLoginResponseDto } from 'src/modules/user/dtos/response/user.login.response.dto'; -import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; -import { UserRefreshResponseDto } from 'src/modules/user/dtos/response/user.refresh.response.dto'; - -export function UserAuthLoginCredentialDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'login with email and password', - }), - DocAuth({ xApiKey: true }), - DocRequest({ - bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, - dto: UserLoginRequestDto, - }), - DocResponse('user.loginWithCredential', { - dto: UserLoginResponseDto, - }) - ); -} - -export function UserAuthLoginSocialGoogleDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'login with social google', - }), - DocAuth({ xApiKey: true, google: true }), - DocResponse('user.loginWithSocialGoogle', { - dto: UserLoginResponseDto, - }) - ); -} - -export function UserAuthLoginSocialAppleDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'login with social apple', - }), - DocAuth({ xApiKey: true, apple: true }), - DocResponse('user.loginWithSocialApple', { - dto: UserLoginResponseDto, - }) - ); -} - -export function UserAuthRefreshDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'refresh a token', - }), - DocAuth({ - xApiKey: true, - jwtRefreshToken: true, - }), - DocResponse('user.refresh', { - dto: UserRefreshResponseDto, - }) - ); -} - -export function UserAuthChangePasswordDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'change password', - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocRequest({ - bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, - dto: UserChangePasswordRequestDto, - }), - DocResponse('user.changePassword') - ); -} - -export function UserAuthProfileDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'get profile', - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocResponse('user.profile', { - dto: UserProfileResponseDto, - }) - ); -} - -export function UserAuthUploadProfileDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'update profile photo', - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocRequestFile({ - dto: FileSingleDto, - }), - DocResponse('user.upload', { - httpStatus: HttpStatus.CREATED, - }) - ); -} - -export function UserAuthUpdateProfileDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'update profile', - }), - DocAuth({ - xApiKey: true, - jwtAccessToken: true, - }), - DocRequest({ - bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, - dto: UserUpdateProfileRequestDto, - }), - DocResponse('user.updateProfile') - ); -} diff --git a/src/modules/user/docs/user.public.doc.ts b/src/modules/user/docs/user.public.doc.ts deleted file mode 100644 index 7efe696d9..000000000 --- a/src/modules/user/docs/user.public.doc.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; -import { - Doc, - DocAuth, - DocRequest, - DocResponse, -} from 'src/common/doc/decorators/doc.decorator'; -import { UserSignUpRequestDto } from 'src/modules/user/dtos/request/user.sign-up.request.dto'; - -export function UserPublicSignUpDoc(): MethodDecorator { - return applyDecorators( - Doc({ - summary: 'sign up', - }), - DocRequest({ - bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, - dto: UserSignUpRequestDto, - }), - DocAuth({ - xApiKey: true, - }), - DocResponse('user.signUp', { - httpStatus: HttpStatus.CREATED, - }) - ); -} diff --git a/src/modules/user/docs/user.shared.doc.ts b/src/modules/user/docs/user.shared.doc.ts new file mode 100644 index 000000000..1b38afcca --- /dev/null +++ b/src/modules/user/docs/user.shared.doc.ts @@ -0,0 +1,62 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + Doc, + DocAuth, + DocRequest, + DocRequestFile, + DocResponse, +} from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { FileSingleDto } from 'src/common/file/dtos/file.single.dto'; +import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.dto'; +import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; + +export function UserSharedProfileDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'get profile', + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocResponse('user.profile', { + dto: UserProfileResponseDto, + }) + ); +} + +export function UserSharedUpdateProfileDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'update profile', + }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: UserUpdateProfileRequestDto, + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocResponse('user.updateProfile') + ); +} + +export function UserSharedUploadProfileDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'update profile photo', + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocRequestFile({ + dto: FileSingleDto, + }), + DocResponse('user.upload', { + httpStatus: HttpStatus.CREATED, + }) + ); +} diff --git a/src/modules/user/docs/user.user.doc.ts b/src/modules/user/docs/user.user.doc.ts index 6f70494c1..1bf8d4e7c 100644 --- a/src/modules/user/docs/user.user.doc.ts +++ b/src/modules/user/docs/user.user.doc.ts @@ -1,5 +1,4 @@ import { applyDecorators } from '@nestjs/common'; -import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; import { Doc, DocAuth, @@ -7,36 +6,56 @@ import { DocRequest, DocResponse, } from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { UserUpdateClaimUsernameRequestDto } from 'src/modules/user/dtos/request/user.update-claim-username.dto'; import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; -export function UserAuthUpdateMobileNumberDoc(): MethodDecorator { +export function UserUserDeleteDoc(): MethodDecorator { return applyDecorators( Doc({ - summary: 'user update mobile number', + summary: 'user delete their account', + }), + DocAuth({ + xApiKey: true, + jwtAccessToken: true, + }), + DocGuard({ role: true }), + DocResponse('user.delete') + ); +} + +export function UserUserUpdateUsernameDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'user update username', }), DocRequest({ bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, - dto: UserUpdateMobileNumberRequestDto, + dto: UserUpdateClaimUsernameRequestDto, }), DocAuth({ xApiKey: true, jwtAccessToken: true, }), DocGuard({ role: true }), - DocResponse('user.updateMobileNumber') + DocResponse('user.updateClaimUsername') ); } -export function UserUserDeleteSelfDoc(): MethodDecorator { +export function UserUserUpdateMobileNumberDoc(): MethodDecorator { return applyDecorators( Doc({ - summary: 'user delete their account', + summary: 'user update mobile number', + }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: UserUpdateMobileNumberRequestDto, }), DocAuth({ xApiKey: true, jwtAccessToken: true, }), DocGuard({ role: true }), - DocResponse('user.deleteSelf') + DocResponse('user.updateMobileNumber') ); } diff --git a/src/modules/user/dtos/request/user.create.request.dto.ts b/src/modules/user/dtos/request/user.create.request.dto.ts index 2190bb0bd..2207d7f88 100644 --- a/src/modules/user/dtos/request/user.create.request.dto.ts +++ b/src/modules/user/dtos/request/user.create.request.dto.ts @@ -20,7 +20,7 @@ export class UserCreateRequestDto { @IsNotEmpty() @MaxLength(100) @Type(() => String) - readonly email: string; + email: string; @ApiProperty({ example: faker.string.uuid(), @@ -28,7 +28,7 @@ export class UserCreateRequestDto { }) @IsNotEmpty() @IsUUID() - readonly role: string; + role: string; @ApiProperty({ example: faker.person.fullName(), @@ -41,7 +41,7 @@ export class UserCreateRequestDto { @MinLength(1) @MaxLength(100) @Type(() => String) - readonly name: string; + name: string; @ApiProperty({ example: faker.string.uuid(), @@ -50,5 +50,5 @@ export class UserCreateRequestDto { @IsString() @IsUUID() @IsNotEmpty() - readonly country: string; + country: string; } diff --git a/src/modules/user/dtos/request/user.update-claim-username.dto.ts b/src/modules/user/dtos/request/user.update-claim-username.dto.ts new file mode 100644 index 000000000..441349210 --- /dev/null +++ b/src/modules/user/dtos/request/user.update-claim-username.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; + +export class UserUpdateClaimUsernameRequestDto { + @IsNotEmpty() + @IsString() + @MaxLength(50) + @MinLength(3) + username: string; +} diff --git a/src/modules/user/dtos/request/user.update-mobile-number.request.dto.ts b/src/modules/user/dtos/request/user.update-mobile-number.request.dto.ts index accb4834b..cb4143645 100644 --- a/src/modules/user/dtos/request/user.update-mobile-number.request.dto.ts +++ b/src/modules/user/dtos/request/user.update-mobile-number.request.dto.ts @@ -22,5 +22,5 @@ export class UserUpdateMobileNumberRequestDto extends PickType( @MinLength(8) @MaxLength(20) @Type(() => String) - readonly number: string; + number: string; } diff --git a/src/modules/user/dtos/request/user.update-password-attempt.request.dto.ts b/src/modules/user/dtos/request/user.update-password-attempt.request.dto.ts index a22533629..5ad61bd6c 100644 --- a/src/modules/user/dtos/request/user.update-password-attempt.request.dto.ts +++ b/src/modules/user/dtos/request/user.update-password-attempt.request.dto.ts @@ -12,5 +12,5 @@ export class UserUpdatePasswordAttemptRequestDto { @Min(0) @Max(3) @Type(() => Number) - readonly passwordAttempt: number; + passwordAttempt: number; } diff --git a/src/modules/user/dtos/request/user.update-profile.request.dto.ts b/src/modules/user/dtos/request/user.update-profile.dto.ts similarity index 95% rename from src/modules/user/dtos/request/user.update-profile.request.dto.ts rename to src/modules/user/dtos/request/user.update-profile.dto.ts index 0a110328a..94d02e78b 100644 --- a/src/modules/user/dtos/request/user.update-profile.request.dto.ts +++ b/src/modules/user/dtos/request/user.update-profile.dto.ts @@ -25,5 +25,7 @@ export class UserUpdateProfileRequestDto extends PickType( required: false, maxLength: 200, }) + @IsString() + @IsOptional() readonly address?: string; } diff --git a/src/modules/user/dtos/request/user.update.request.dto.ts b/src/modules/user/dtos/request/user.update.request.dto.ts new file mode 100644 index 000000000..995da388f --- /dev/null +++ b/src/modules/user/dtos/request/user.update.request.dto.ts @@ -0,0 +1,8 @@ +import { PickType } from '@nestjs/swagger'; +import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; + +export class UserUpdateRequestDto extends PickType(UserCreateRequestDto, [ + 'name', + 'country', + 'role', +] as const) {} diff --git a/src/modules/user/dtos/response/user-login-history.list.response.dto.ts b/src/modules/user/dtos/response/user-login-history.list.response.dto.ts deleted file mode 100644 index 3b5101e57..000000000 --- a/src/modules/user/dtos/response/user-login-history.list.response.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude } from 'class-transformer'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; - -export class UserLoginHistoryListResponseDto extends DatabaseIdResponseDto { - @ApiProperty({ - required: true, - example: faker.string.uuid(), - }) - readonly user: string; - - @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; -} diff --git a/src/modules/user/dtos/response/user-password-history.list.response.dto.ts b/src/modules/user/dtos/response/user-password-history.list.response.dto.ts deleted file mode 100644 index 6be889846..000000000 --- a/src/modules/user/dtos/response/user-password-history.list.response.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude } from 'class-transformer'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; - -export class UserPasswordHistoryListResponseDto extends DatabaseIdResponseDto { - @ApiProperty({ - required: true, - example: faker.string.uuid(), - }) - readonly user: string; - - @ApiHideProperty() - @Exclude() - readonly password: string; - - @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; -} diff --git a/src/modules/user/dtos/response/user-state-history.list.response.dto.ts b/src/modules/user/dtos/response/user-state-history.list.response.dto.ts deleted file mode 100644 index 560f59474..000000000 --- a/src/modules/user/dtos/response/user-state-history.list.response.dto.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Exclude } from 'class-transformer'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; -import { ENUM_USER_HISTORY_STATE } from 'src/modules/user/constants/user-history.enum.constant'; - -export class UserStateHistoryListResponseDto extends DatabaseIdResponseDto { - @ApiProperty({ - required: true, - example: faker.string.uuid(), - }) - readonly user: string; - - @ApiProperty({ - required: true, - enum: ENUM_USER_HISTORY_STATE, - example: ENUM_USER_HISTORY_STATE.ACTIVE, - }) - readonly beforeState: ENUM_USER_HISTORY_STATE; - - @ApiProperty({ - required: true, - enum: ENUM_USER_HISTORY_STATE, - example: ENUM_USER_HISTORY_STATE.ACTIVE, - }) - readonly afterState: ENUM_USER_HISTORY_STATE; - - @ApiProperty({ - required: true, - example: faker.string.uuid(), - }) - readonly by: string; - - @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, - }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; -} diff --git a/src/modules/user/dtos/response/user.get.response.dto.ts b/src/modules/user/dtos/response/user.get.response.dto.ts index fbf68ad57..69a63f06b 100644 --- a/src/modules/user/dtos/response/user.get.response.dto.ts +++ b/src/modules/user/dtos/response/user.get.response.dto.ts @@ -1,37 +1,38 @@ import { faker } from '@faker-js/faker'; import { ApiHideProperty, ApiProperty, getSchemaPath } from '@nestjs/swagger'; import { Exclude, Type } from 'class-transformer'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; -import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; import { ENUM_USER_GENDER, ENUM_USER_SIGN_UP_FROM, ENUM_USER_STATUS, -} from 'src/modules/user/constants/user.enum.constant'; +} from 'src/modules/user/enums/user.enum'; import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; -export class UserGetResponseDto extends DatabaseIdResponseDto { +export class UserGetResponseDto extends DatabaseDto { @ApiProperty({ required: true, nullable: false, maxLength: 100, minLength: 1, }) - readonly name: string; + name: string; @ApiProperty({ - required: false, + required: true, + nullable: false, maxLength: 50, - minLength: 1, + minLength: 3, }) - readonly familyName?: string; + username: string; @ApiProperty({ required: false, - type: () => UserUpdateMobileNumberRequestDto, + type: UserUpdateMobileNumberRequestDto, }) @Type(() => UserUpdateMobileNumberRequestDto) - readonly mobileNumber?: UserUpdateMobileNumberRequestDto; + mobileNumber?: UserUpdateMobileNumberRequestDto; @ApiProperty({ required: true, @@ -39,83 +40,70 @@ export class UserGetResponseDto extends DatabaseIdResponseDto { example: faker.internet.email(), maxLength: 100, }) - readonly email: string; + email: string; @ApiProperty({ required: true, nullable: false, example: faker.string.uuid(), }) - readonly role: string; + role: string; @ApiHideProperty() @Exclude() - readonly password: string; + password: string; @ApiProperty({ required: true, nullable: false, example: faker.date.future(), }) - readonly passwordExpired: Date; + passwordExpired: Date; @ApiProperty({ required: true, nullable: false, example: faker.date.past(), }) - readonly passwordCreated: Date; + passwordCreated: Date; @ApiHideProperty() @Exclude() - readonly passwordAttempt: number; + passwordAttempt: number; @ApiProperty({ required: true, nullable: false, example: faker.date.recent(), }) - readonly signUpDate: Date; + signUpDate: Date; @ApiProperty({ required: true, nullable: false, example: ENUM_USER_SIGN_UP_FROM.ADMIN, }) - readonly signUpFrom: ENUM_USER_SIGN_UP_FROM; + signUpFrom: ENUM_USER_SIGN_UP_FROM; @ApiHideProperty() @Exclude() - readonly salt: string; + salt: string; @ApiProperty({ required: true, nullable: false, example: ENUM_USER_STATUS.ACTIVE, }) - readonly status: ENUM_USER_STATUS; - - @ApiProperty({ - required: true, - nullable: false, - example: false, - }) - readonly blocked: boolean; + status: ENUM_USER_STATUS; @ApiProperty({ nullable: true, required: false, - type: () => AwsS3Dto, + type: AwsS3Dto, oneOf: [{ $ref: getSchemaPath(AwsS3Dto) }], }) @Type(() => AwsS3Dto) - readonly photo?: AwsS3Dto; - - @ApiProperty({ - required: false, - nullable: true, - }) - readonly address?: string; + photo?: AwsS3Dto; @ApiProperty({ example: ENUM_USER_GENDER.MALE, @@ -123,31 +111,27 @@ export class UserGetResponseDto extends DatabaseIdResponseDto { required: false, nullable: true, }) - readonly gender?: ENUM_USER_GENDER; + gender?: ENUM_USER_GENDER; @ApiProperty({ example: faker.string.uuid(), required: true, }) - readonly country: string; + country: string; @ApiProperty({ - description: 'Date created at', - example: faker.date.recent(), - required: true, - nullable: false, + example: faker.location.streetAddress(), + required: false, + nullable: true, + maxLength: 200, }) - readonly createdAt: Date; + address?: string; @ApiProperty({ - description: 'Date updated at', - example: faker.date.recent(), - required: true, - nullable: false, + example: faker.person.lastName(), + required: false, + nullable: true, + maxLength: 50, }) - readonly updatedAt: Date; - - @ApiHideProperty() - @Exclude() - readonly deletedAt?: Date; + familyName?: string; } diff --git a/src/modules/user/dtos/response/user.list.response.dto.ts b/src/modules/user/dtos/response/user.list.response.dto.ts index eaa0efa8f..17ec36b1a 100644 --- a/src/modules/user/dtos/response/user.list.response.dto.ts +++ b/src/modules/user/dtos/response/user.list.response.dto.ts @@ -1,22 +1,25 @@ import { ApiHideProperty, ApiProperty, OmitType } from '@nestjs/swagger'; import { Exclude, Type } from 'class-transformer'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; import { ENUM_USER_GENDER, ENUM_USER_SIGN_UP_FROM, -} from 'src/modules/user/constants/user.enum.constant'; +} from 'src/modules/user/enums/user.enum'; +import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; -import { UserMobileNumberResponseDto } from 'src/modules/user/dtos/response/user.mobile-number.response.dto'; export class UserListResponseDto extends OmitType(UserGetResponseDto, [ 'passwordExpired', 'passwordCreated', 'signUpDate', 'signUpFrom', - 'address', 'gender', 'role', + 'country', 'mobileNumber', + 'address', + 'familyName', ] as const) { @ApiProperty({ required: true, @@ -24,37 +27,45 @@ export class UserListResponseDto extends OmitType(UserGetResponseDto, [ type: RoleListResponseDto, }) @Type(() => RoleListResponseDto) - readonly role: RoleListResponseDto; + role: RoleListResponseDto; @ApiProperty({ - required: false, + required: true, nullable: false, - type: UserMobileNumberResponseDto, + type: CountryShortResponseDto, }) - @Type(() => RoleListResponseDto) - readonly mobileNumber?: UserMobileNumberResponseDto; + @Type(() => CountryShortResponseDto) + country: CountryShortResponseDto; + + @ApiHideProperty() + @Exclude() + mobileNumber?: UserUpdateMobileNumberRequestDto; + + @ApiHideProperty() + @Exclude() + passwordExpired: Date; @ApiHideProperty() @Exclude() - readonly passwordExpired: Date; + passwordCreated: Date; @ApiHideProperty() @Exclude() - readonly passwordCreated: Date; + signUpDate: Date; @ApiHideProperty() @Exclude() - readonly signUpDate: Date; + signUpFrom: ENUM_USER_SIGN_UP_FROM; @ApiHideProperty() @Exclude() - readonly signUpFrom: ENUM_USER_SIGN_UP_FROM; + gender?: ENUM_USER_GENDER; @ApiHideProperty() @Exclude() - readonly address?: string; + address?: string; @ApiHideProperty() @Exclude() - readonly gender?: ENUM_USER_GENDER; + familyName?: string; } diff --git a/src/modules/user/dtos/response/user.mobile-number.response.dto.ts b/src/modules/user/dtos/response/user.mobile-number.response.dto.ts index a0a521c99..1f17e5925 100644 --- a/src/modules/user/dtos/response/user.mobile-number.response.dto.ts +++ b/src/modules/user/dtos/response/user.mobile-number.response.dto.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; export class UserMobileNumberResponseDto { @ApiProperty({ @@ -14,13 +14,13 @@ export class UserMobileNumberResponseDto { minLength: 8, }) @Type(() => String) - readonly number: string; + number: string; @ApiProperty({ required: true, nullable: false, - type: CountryGetResponseDto, + type: CountryShortResponseDto, }) - @Type(() => CountryGetResponseDto) - readonly country: CountryGetResponseDto; + @Type(() => CountryShortResponseDto) + country: CountryShortResponseDto; } diff --git a/src/modules/user/dtos/response/user.profile.response.dto.ts b/src/modules/user/dtos/response/user.profile.response.dto.ts index f420590ab..f61742a25 100644 --- a/src/modules/user/dtos/response/user.profile.response.dto.ts +++ b/src/modules/user/dtos/response/user.profile.response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; import { RoleGetResponseDto } from 'src/modules/role/dtos/response/role.get.response.dto'; import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; @@ -17,15 +17,15 @@ export class UserProfileResponseDto extends OmitType(UserGetResponseDto, [ type: RoleGetResponseDto, }) @Type(() => RoleGetResponseDto) - readonly role: RoleGetResponseDto; + role: RoleGetResponseDto; @ApiProperty({ required: true, nullable: false, - type: CountryGetResponseDto, + type: CountryShortResponseDto, }) - @Type(() => CountryGetResponseDto) - readonly country: CountryGetResponseDto; + @Type(() => CountryShortResponseDto) + country: CountryShortResponseDto; @ApiProperty({ required: false, @@ -33,5 +33,5 @@ export class UserProfileResponseDto extends OmitType(UserGetResponseDto, [ type: UserMobileNumberResponseDto, }) @Type(() => RoleListResponseDto) - readonly mobileNumber?: UserMobileNumberResponseDto; + mobileNumber?: UserMobileNumberResponseDto; } diff --git a/src/modules/user/dtos/response/user.refresh.response.dto.ts b/src/modules/user/dtos/response/user.refresh.response.dto.ts deleted file mode 100644 index 769178fbf..000000000 --- a/src/modules/user/dtos/response/user.refresh.response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UserLoginResponseDto } from 'src/modules/user/dtos/response/user.login.response.dto'; - -export class UserRefreshResponseDto extends UserLoginResponseDto {} diff --git a/src/modules/user/dtos/response/user.short.response.dto.ts b/src/modules/user/dtos/response/user.short.response.dto.ts new file mode 100644 index 000000000..6c3200ce2 --- /dev/null +++ b/src/modules/user/dtos/response/user.short.response.dto.ts @@ -0,0 +1,34 @@ +import { ApiHideProperty, OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { RoleListResponseDto } from 'src/modules/role/dtos/response/role.list.response.dto'; +import { ENUM_USER_STATUS } from 'src/modules/user/enums/user.enum'; +import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; + +export class UserShortResponseDto extends OmitType(UserListResponseDto, [ + 'role', + 'status', + 'photo', + 'createdAt', + 'updatedAt', +]) { + @ApiHideProperty() + @Exclude() + role: RoleListResponseDto; + + @ApiHideProperty() + @Exclude() + status: ENUM_USER_STATUS; + + @ApiHideProperty() + @Exclude() + photo?: AwsS3Dto; + + @ApiHideProperty() + @Exclude() + createdAt: Date; + + @ApiHideProperty() + @Exclude() + updatedAt: Date; +} diff --git a/src/modules/user/constants/user.enum.constant.ts b/src/modules/user/enums/user.enum.ts similarity index 79% rename from src/modules/user/constants/user.enum.constant.ts rename to src/modules/user/enums/user.enum.ts index c9329378e..a42a008a9 100644 --- a/src/modules/user/constants/user.enum.constant.ts +++ b/src/modules/user/enums/user.enum.ts @@ -1,12 +1,15 @@ export enum ENUM_USER_SIGN_UP_FROM { ADMIN = 'ADMIN', PUBLIC = 'PUBLIC', + SEED = 'SEED', } export enum ENUM_USER_STATUS { + CREATED = 'CREATED', ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', DELETED = 'DELETED', + BLOCKED = 'BLOCKED', } export enum ENUM_USER_GENDER { diff --git a/src/modules/user/enums/user.status-code.enum.ts b/src/modules/user/enums/user.status-code.enum.ts new file mode 100644 index 000000000..24eca6eec --- /dev/null +++ b/src/modules/user/enums/user.status-code.enum.ts @@ -0,0 +1,16 @@ +export enum ENUM_USER_STATUS_CODE_ERROR { + NOT_FOUND = 5150, + NOT_SELF = 5151, + EMAIL_EXIST = 5152, + USERNAME_EXIST = 5153, + MOBILE_NUMBER_EXIST = 5154, + STATUS_INVALID = 5155, + BLOCKED_INVALID = 5156, + INACTIVE_FORBIDDEN = 5157, + DELETED_FORBIDDEN = 5158, + BLOCKED_FORBIDDEN = 5159, + PASSWORD_NOT_MATCH = 5160, + PASSWORD_MUST_NEW = 5161, + PASSWORD_EXPIRED = 5162, + PASSWORD_ATTEMPT_MAX = 5163, +} diff --git a/src/modules/user/guards/user.guard.ts b/src/modules/user/guards/user.guard.ts deleted file mode 100644 index 850ce6ee0..000000000 --- a/src/modules/user/guards/user.guard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; -import { UserService } from 'src/modules/user/services/user.service'; - -@Injectable() -export class UserGuard implements CanActivate { - constructor(private readonly userService: UserService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - - const user = await this.userService.findOneByEmailAndActive( - request.user._id - ); - if (!user) { - throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'user.error.notFound', - }); - } - - request.__user = user; - return true; - } -} diff --git a/src/modules/user/interfaces/user-login-history.service.interface.ts b/src/modules/user/interfaces/user-login-history.service.interface.ts deleted file mode 100644 index d3693df08..000000000 --- a/src/modules/user/interfaces/user-login-history.service.interface.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Request } from 'express'; -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { UserLoginHistoryCreateRequest } from 'src/modules/user/dtos/request/user-login-history.create.request.dto'; -import { UserLoginHistoryListResponseDto } from 'src/modules/user/dtos/response/user-login-history.list.response.dto'; -import { UserLoginHistoryDoc } from 'src/modules/user/repository/entities/user-login-history.entity'; - -export interface IUserLoginHistoryService { - findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - getTotalByUser( - user: string, - options?: IDatabaseGetTotalOptions - ): Promise; - create( - request: Request, - { user }: UserLoginHistoryCreateRequest, - options?: IDatabaseCreateOptions - ): Promise; - mapList( - userHistories: UserLoginHistoryDoc[] - ): Promise; -} diff --git a/src/modules/user/interfaces/user-password-history.service.interface.ts b/src/modules/user/interfaces/user-password-history.service.interface.ts deleted file mode 100644 index cf7910940..000000000 --- a/src/modules/user/interfaces/user-password-history.service.interface.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { UserPasswordHistoryListResponseDto } from 'src/modules/user/dtos/response/user-password-history.list.response.dto'; -import { UserPasswordHistoryDoc } from 'src/modules/user/repository/entities/user-password-history.entity'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; - -export interface IUserPasswordHistoryService { - findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByUser( - user: UserDoc, - password: IAuthPassword, - options?: IDatabaseFindOneOptions - ): Promise; - getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - getTotalByUser( - user: string, - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - createByUser( - user: UserDoc, - options?: IDatabaseCreateOptions - ): Promise; - createByAdmin( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - mapList( - userHistories: UserPasswordHistoryDoc[] - ): Promise; - checkPasswordPeriodByUser( - user: UserDoc, - password: IAuthPassword, - options?: IDatabaseFindOneOptions - ): Promise; - getPasswordPeriod(): Promise; -} diff --git a/src/modules/user/interfaces/user-state-history.service.interface.ts b/src/modules/user/interfaces/user-state-history.service.interface.ts deleted file mode 100644 index fa3aad1dc..000000000 --- a/src/modules/user/interfaces/user-state-history.service.interface.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { ENUM_USER_HISTORY_STATE } from 'src/modules/user/constants/user-history.enum.constant'; -import { UserStateHistoryListResponseDto } from 'src/modules/user/dtos/response/user-state-history.list.response.dto'; -import { UserStateHistoryDoc } from 'src/modules/user/repository/entities/user-state-history.entity'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; - -export interface IUserStateHistoryService { - findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - getTotalByUser( - user: string, - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise; - setState(user: UserDoc): Promise; - createCreated( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - createActive( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - createInactive( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - createBlocked( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - createDeleted( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise; - mapList( - userHistories: UserStateHistoryDoc[] - ): Promise; -} diff --git a/src/modules/user/interfaces/user.service.interface.ts b/src/modules/user/interfaces/user.service.interface.ts index 254912602..60e827b8a 100644 --- a/src/modules/user/interfaces/user.service.interface.ts +++ b/src/modules/user/interfaces/user.service.interface.ts @@ -1,58 +1,88 @@ -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; +import { IAuthPassword } from 'src/modules/auth/interfaces/auth.interface'; import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseExistOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, + IDatabaseUpdateOptions, } from 'src/common/database/interfaces/database.interface'; -import { ENUM_USER_SIGN_UP_FROM } from 'src/modules/user/constants/user.enum.constant'; -import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; -import { UserSignUpRequestDto } from 'src/modules/user/dtos/request/user.sign-up.request.dto'; -import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; -import { UserUpdatePasswordAttemptRequestDto } from 'src/modules/user/dtos/request/user.update-password-attempt.request.dto'; -import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.request.dto'; -import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; -import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; -import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; -import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; import { UserDoc, UserEntity, } from 'src/modules/user/repository/entities/user.entity'; +import { + IUserDoc, + IUserEntity, +} from 'src/modules/user/interfaces/user.interface'; +import { UserUpdatePasswordAttemptRequestDto } from 'src/modules/user/dtos/request/user.update-password-attempt.request.dto'; +import { ENUM_USER_SIGN_UP_FROM } from 'src/modules/user/enums/user.enum'; +import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; +import { UserUpdateRequestDto } from 'src/modules/user/dtos/request/user.update.request.dto'; +import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; +import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; +import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; +import { UserShortResponseDto } from 'src/modules/user/dtos/response/user.short.response.dto'; +import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; +import { AuthSignUpRequestDto } from 'src/modules/auth/dtos/request/auth.sign-up.request.dto'; +import { UserUpdateClaimUsernameRequestDto } from 'src/modules/user/dtos/request/user.update-claim-username.dto'; +import { DatabaseSoftDeleteDto } from 'src/common/database/dtos/database.soft-delete.dto'; +import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.dto'; export interface IUserService { findAll( find?: Record, options?: IDatabaseFindAllOptions ): Promise; - findAllWithRoleAndCountry( + getTotal( find?: Record, - options?: IDatabaseFindAllOptions - ): Promise; - findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; + options?: IDatabaseGetTotalOptions + ): Promise; + findOneById(_id: string, options?: IDatabaseOptions): Promise; findOne( find: Record, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByEmail( - email: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; + findOneByEmail(email: string, options?: IDatabaseOptions): Promise; findOneByMobileNumber( mobileNumber: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise; - getTotal( + findAllWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + findOneWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + findOneWithRoleAndCountryById( + _id: string, + options?: IDatabaseFindAllOptions + ): Promise; + findAllActiveWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + getTotalActive( find?: Record, options?: IDatabaseGetTotalOptions ): Promise; + findOneActiveById( + _id: string, + options?: IDatabaseOptions + ): Promise; + findOneActiveByEmail( + email: string, + options?: IDatabaseOptions + ): Promise; + findOneActiveByMobileNumber( + mobileNumber: string, + options?: IDatabaseOptions + ): Promise; create( { email, name, role, country }: UserCreateRequestDto, { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, @@ -61,7 +91,7 @@ export interface IUserService { ): Promise; signUp( role: string, - { email, name, country }: UserSignUpRequestDto, + { email, name, country }: AuthSignUpRequestDto, { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, options?: IDatabaseCreateOptions ): Promise; @@ -69,6 +99,10 @@ export interface IUserService { email: string, options?: IDatabaseExistOptions ): Promise; + existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise; existByMobileNumber( mobileNumber: string, options?: IDatabaseExistOptions @@ -91,18 +125,10 @@ export interface IUserService { repository: UserDoc, options?: IDatabaseSaveOptions ): Promise; - selfDelete( - repository: UserDoc, - options?: IDatabaseSaveOptions - ): Promise; blocked( repository: UserDoc, options?: IDatabaseSaveOptions ): Promise; - unblocked( - repository: UserDoc, - options?: IDatabaseSaveOptions - ): Promise; updatePasswordAttempt( repository: UserDoc, { passwordAttempt }: UserUpdatePasswordAttemptRequestDto, @@ -110,7 +136,7 @@ export interface IUserService { ): Promise; increasePasswordAttempt( repository: UserDoc, - options?: IDatabaseSaveOptions + options?: IDatabaseUpdateOptions ): Promise; resetPasswordAttempt( repository: UserDoc, @@ -121,28 +147,9 @@ export interface IUserService { passwordExpired: Date, options?: IDatabaseSaveOptions ): Promise; - join(repository: UserDoc): Promise; - getPhotoUploadPath(user: string): Promise; - deleteMany( - find: Record, - options?: IDatabaseManyOptions - ): Promise; - findOneByIdAndActive( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByEmailAndActive( - email: string, - options?: IDatabaseFindOneOptions - ): Promise; - findOneByMobileNumberAndActive( - mobileNumber: string, - options?: IDatabaseFindOneOptions - ): Promise; - mapProfile(user: IUserDoc): Promise; - updateProfile( + update( repository: UserDoc, - { name, familyName, address }: UserUpdateProfileRequestDto, + { country, name, role }: UserUpdateRequestDto, options?: IDatabaseSaveOptions ): Promise; updateMobileNumber( @@ -150,10 +157,38 @@ export interface IUserService { { country, number }: UserUpdateMobileNumberRequestDto, options?: IDatabaseSaveOptions ): Promise; - deleteMobileNumber( + updateClaimUsername( + repository: UserDoc, + { username }: UserUpdateClaimUsernameRequestDto, + options?: IDatabaseSaveOptions + ): Promise; + removeMobileNumber( + repository: UserDoc, + options?: IDatabaseSaveOptions + ): Promise; + delete( repository: UserDoc, + dto: DatabaseSoftDeleteDto, options?: IDatabaseSaveOptions ): Promise; - mapList(user: IUserDoc[]): Promise; - mapGet(user: IUserDoc): Promise; + deleteMany( + find: Record, + options?: IDatabaseDeleteManyOptions + ): Promise; + updateProfile( + repository: UserDoc, + { country, name, address, familyName }: UserUpdateProfileRequestDto, + options?: IDatabaseSaveOptions + ): Promise; + join(repository: UserDoc): Promise; + getPhotoUploadPath(user: string): Promise; + mapProfile(user: IUserDoc | IUserEntity): Promise; + createRandomFilenamePhoto(): Promise; + createRandomUsername(): Promise; + checkUsername(username: string): Promise; + mapList(users: IUserDoc[] | IUserEntity[]): Promise; + mapShort( + users: IUserDoc[] | IUserEntity[] + ): Promise; + mapGet(user: IUserDoc | IUserEntity): Promise; } diff --git a/src/modules/user/pipes/user.blocked.pipe.ts b/src/modules/user/pipes/user.blocked.pipe.ts deleted file mode 100644 index 00d229390..000000000 --- a/src/modules/user/pipes/user.blocked.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; - -@Injectable() -export class UserNotBlockedPipe implements PipeTransform { - async transform(value: UserDoc): Promise { - if (value.blocked) { - throw new BadRequestException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.BLOCKED_INVALID_ERROR, - message: 'user.error.blockedInvalid', - }); - } - - return value; - } -} diff --git a/src/modules/user/pipes/user.not-self.pipe.ts b/src/modules/user/pipes/user.not-self.pipe.ts index f1c440625..03cda678a 100644 --- a/src/modules/user/pipes/user.not-self.pipe.ts +++ b/src/modules/user/pipes/user.not-self.pipe.ts @@ -1,13 +1,14 @@ import { + BadRequestException, Inject, Injectable, - NotFoundException, PipeTransform, Scope, } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; import { IRequestApp } from 'src/common/request/interfaces/request.interface'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; @Injectable({ scope: Scope.REQUEST }) export class UserNotSelfPipe implements PipeTransform { @@ -15,10 +16,13 @@ export class UserNotSelfPipe implements PipeTransform { async transform(value: string): Promise { const { user } = this.request; - if (user.user_id === value) { - throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, - message: 'user.error.notFound', + if ( + user._id === value && + user.type !== ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN + ) { + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_SELF, + message: 'user.error.notSelf', }); } diff --git a/src/modules/user/pipes/user.parse.pipe.ts b/src/modules/user/pipes/user.parse.pipe.ts index bc4642c78..aad093756 100644 --- a/src/modules/user/pipes/user.parse.pipe.ts +++ b/src/modules/user/pipes/user.parse.pipe.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; import { UserService } from 'src/modules/user/services/user.service'; @@ -7,11 +8,29 @@ import { UserService } from 'src/modules/user/services/user.service'; export class UserParsePipe implements PipeTransform { constructor(private readonly userService: UserService) {} - async transform(value: any): Promise { + async transform(value: string): Promise { const user: UserDoc = await this.userService.findOneById(value); if (!user) { throw new NotFoundException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND_ERROR, + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND, + message: 'user.error.notFound', + }); + } + + return user; + } +} + +@Injectable() +export class UserActiveParsePipe implements PipeTransform { + constructor(private readonly userService: UserService) {} + + async transform(value: string): Promise { + const user = + await this.userService.findOneWithRoleAndCountryById(value); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.NOT_FOUND, message: 'user.error.notFound', }); } diff --git a/src/modules/user/pipes/user.status.pipe.ts b/src/modules/user/pipes/user.status.pipe.ts index 5aa5749da..936c93b4e 100644 --- a/src/modules/user/pipes/user.status.pipe.ts +++ b/src/modules/user/pipes/user.status.pipe.ts @@ -1,28 +1,19 @@ import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; -import { ENUM_USER_STATUS } from 'src/modules/user/constants/user.enum.constant'; -import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { ENUM_USER_STATUS } from 'src/modules/user/enums/user.enum'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/enums/user.status-code.enum'; import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; - @Injectable() -export class UserStatusActivePipe implements PipeTransform { - async transform(value: UserDoc): Promise { - if (value.status === ENUM_USER_STATUS.ACTIVE) { - throw new BadRequestException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.STATUS_INVALID_ERROR, - message: 'user.error.statusInvalid', - }); - } +export class UserStatusPipe implements PipeTransform { + private readonly status: ENUM_USER_STATUS[]; - return value; + constructor(status: ENUM_USER_STATUS[]) { + this.status = status; } -} -@Injectable() -export class UserStatusInactivePipe implements PipeTransform { async transform(value: UserDoc): Promise { - if (value.status === ENUM_USER_STATUS.ACTIVE) { + if (!this.status.includes(value.status)) { throw new BadRequestException({ - statusCode: ENUM_USER_STATUS_CODE_ERROR.STATUS_INVALID_ERROR, + statusCode: ENUM_USER_STATUS_CODE_ERROR.STATUS_INVALID, message: 'user.error.statusInvalid', }); } diff --git a/src/modules/user/repository/entities/user-login-history.entity.ts b/src/modules/user/repository/entities/user-login-history.entity.ts deleted file mode 100644 index b15fec484..000000000 --- a/src/modules/user/repository/entities/user-login-history.entity.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; -import { - DatabaseEntity, - DatabaseProp, - DatabaseSchema, -} from 'src/common/database/decorators/database.decorator'; -import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; -import { UserEntity } from 'src/modules/user/repository/entities/user.entity'; - -export const UserLoginHistoryTableName = 'UserLoginHistories'; - -@DatabaseEntity({ collection: UserLoginHistoryTableName }) -export class UserLoginHistoryEntity extends DatabaseMongoUUIDEntityAbstract { - @DatabaseProp({ - required: true, - index: true, - trim: true, - type: String, - ref: UserEntity.name, - }) - user: string; - - @DatabaseProp({ - required: true, - trim: true, - type: String, - }) - ip: string; - - @DatabaseProp({ - required: true, - trim: true, - type: String, - }) - hostname: string; - - @DatabaseProp({ - required: true, - trim: true, - type: String, - }) - protocol: string; - - @DatabaseProp({ - required: true, - trim: true, - type: String, - }) - originalUrl: string; - - @DatabaseProp({ - required: true, - trim: true, - type: String, - }) - method: string; - - @DatabaseProp({ - required: false, - trim: true, - type: String, - }) - userAgent?: string; - - @DatabaseProp({ - required: false, - trim: true, - type: String, - }) - xForwardedFor?: string; - - @DatabaseProp({ - required: false, - trim: true, - type: String, - }) - xForwardedHost?: string; - - @DatabaseProp({ - required: false, - trim: true, - type: String, - }) - xForwardedPorto?: string; -} - -export const UserLoginHistorySchema = DatabaseSchema(UserLoginHistoryEntity); -export type UserLoginHistoryDoc = IDatabaseDocument; diff --git a/src/modules/user/repository/entities/user-password-history.entity.ts b/src/modules/user/repository/entities/user-password-history.entity.ts deleted file mode 100644 index 1539dc223..000000000 --- a/src/modules/user/repository/entities/user-password-history.entity.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; -import { - DatabaseEntity, - DatabaseProp, - DatabaseSchema, -} from 'src/common/database/decorators/database.decorator'; -import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; -import { UserEntity } from 'src/modules/user/repository/entities/user.entity'; - -export const UserPasswordHistoryTableName = 'UserPasswordHistories'; - -@DatabaseEntity({ collection: UserPasswordHistoryTableName }) -export class UserPasswordHistoryEntity extends DatabaseMongoUUIDEntityAbstract { - @DatabaseProp({ - required: true, - index: true, - trim: true, - type: String, - ref: UserEntity.name, - }) - user: string; - - @DatabaseProp({ - required: true, - type: String, - }) - password: string; - - @DatabaseProp({ - required: true, - index: true, - trim: true, - type: String, - ref: UserEntity.name, - }) - by: string; -} - -export const UserPasswordHistorySchema = DatabaseSchema( - UserPasswordHistoryEntity -); -export type UserPasswordHistoryDoc = - IDatabaseDocument; diff --git a/src/modules/user/repository/entities/user-state-history.entity.ts b/src/modules/user/repository/entities/user-state-history.entity.ts deleted file mode 100644 index 2d04136db..000000000 --- a/src/modules/user/repository/entities/user-state-history.entity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; -import { - DatabaseEntity, - DatabaseProp, - DatabaseSchema, -} from 'src/common/database/decorators/database.decorator'; -import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; -import { ENUM_USER_HISTORY_STATE } from 'src/modules/user/constants/user-history.enum.constant'; -import { UserEntity } from 'src/modules/user/repository/entities/user.entity'; - -export const UserStateHistoryTableName = 'UserStateHistories'; - -@DatabaseEntity({ collection: UserStateHistoryTableName }) -export class UserStateHistoryEntity extends DatabaseMongoUUIDEntityAbstract { - @DatabaseProp({ - required: true, - index: true, - trim: true, - type: String, - ref: UserEntity.name, - }) - user: string; - - @DatabaseProp({ - required: true, - type: String, - enum: ENUM_USER_HISTORY_STATE, - }) - beforeState: ENUM_USER_HISTORY_STATE; - - @DatabaseProp({ - required: true, - type: String, - enum: ENUM_USER_HISTORY_STATE, - }) - afterState: ENUM_USER_HISTORY_STATE; - - @DatabaseProp({ - required: true, - index: true, - trim: true, - type: String, - ref: UserEntity.name, - }) - by: string; -} - -export const UserStateHistorySchema = DatabaseSchema(UserStateHistoryEntity); -export type UserStateHistoryDoc = IDatabaseDocument; diff --git a/src/modules/user/repository/entities/user.entity.ts b/src/modules/user/repository/entities/user.entity.ts index f72291522..6c10c7134 100644 --- a/src/modules/user/repository/entities/user.entity.ts +++ b/src/modules/user/repository/entities/user.entity.ts @@ -1,21 +1,21 @@ -import { - AwsS3Entity, - AwsS3Schema, -} from 'src/common/aws/repository/entities/aws.s3.entity'; -import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntityAbstract } from 'src/common/database/abstracts/database.entity.abstract'; import { DatabaseEntity, DatabaseProp, DatabaseSchema, } from 'src/common/database/decorators/database.decorator'; import { IDatabaseDocument } from 'src/common/database/interfaces/database.interface'; +import { + AwsS3Entity, + AwsS3Schema, +} from 'src/modules/aws/repository/entities/aws.s3.entity'; import { CountryEntity } from 'src/modules/country/repository/entities/country.entity'; import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; import { ENUM_USER_GENDER, ENUM_USER_SIGN_UP_FROM, ENUM_USER_STATUS, -} from 'src/modules/user/constants/user.enum.constant'; +} from 'src/modules/user/enums/user.enum'; export const UserTableName = 'Users'; @@ -28,6 +28,7 @@ export class UserMobileNumberEntity { required: true, type: String, ref: CountryEntity.name, + trim: true, }) country: string; @@ -45,7 +46,7 @@ export const UserMobileNumberSchema = DatabaseSchema(UserMobileNumberEntity); export type UserMobileNumberDoc = IDatabaseDocument; @DatabaseEntity({ collection: UserTableName }) -export class UserEntity extends DatabaseMongoUUIDEntityAbstract { +export class UserEntity extends DatabaseEntityAbstract { @DatabaseProp({ required: true, index: true, @@ -56,12 +57,15 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { name: string; @DatabaseProp({ - required: false, + required: true, + index: true, trim: true, type: String, maxlength: 50, + minlength: 3, + unique: true, }) - familyName?: string; + username: string; @DatabaseProp({ required: false, @@ -74,7 +78,6 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { unique: true, index: true, trim: true, - lowercase: true, type: String, maxlength: 100, }) @@ -84,12 +87,14 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { required: true, ref: RoleEntity.name, index: true, + trim: true, }) role: string; @DatabaseProp({ required: true, type: String, + trim: true, }) password: string; @@ -115,6 +120,7 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { @DatabaseProp({ required: true, type: Date, + trim: true, }) signUpDate: Date; @@ -139,14 +145,6 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { }) status: ENUM_USER_STATUS; - @DatabaseProp({ - required: true, - default: false, - index: true, - type: Boolean, - }) - blocked: boolean; - @DatabaseProp({ required: false, schema: AwsS3Schema, @@ -155,27 +153,31 @@ export class UserEntity extends DatabaseMongoUUIDEntityAbstract { @DatabaseProp({ required: false, - maxlength: 200, + enum: ENUM_USER_GENDER, }) - address?: string; + gender?: ENUM_USER_GENDER; @DatabaseProp({ - required: false, - enum: ENUM_USER_GENDER, + required: true, + type: String, + ref: CountryEntity.name, + trim: true, }) - gender?: ENUM_USER_GENDER; + country: string; @DatabaseProp({ required: false, + maxlength: 200, + trim: true, }) - selfDeletion?: boolean; + address?: string; @DatabaseProp({ - required: true, - type: String, - ref: CountryEntity.name, + required: false, + maxlength: 50, + trim: true, }) - country: string; + familyName?: string; } export const UserSchema = DatabaseSchema(UserEntity); diff --git a/src/modules/user/repository/repositories/user-login-history.repository.ts b/src/modules/user/repository/repositories/user-login-history.repository.ts deleted file mode 100644 index 0ef358148..000000000 --- a/src/modules/user/repository/repositories/user-login-history.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; -import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; -import { - UserLoginHistoryDoc, - UserLoginHistoryEntity, -} from 'src/modules/user/repository/entities/user-login-history.entity'; - -@Injectable() -export class UserLoginHistoryRepository extends DatabaseMongoUUIDRepositoryAbstract< - UserLoginHistoryEntity, - UserLoginHistoryDoc -> { - constructor( - @DatabaseModel(UserLoginHistoryEntity.name) - private readonly userLoginHistoryModel: Model - ) { - super(userLoginHistoryModel); - } -} diff --git a/src/modules/user/repository/repositories/user-password-history.repository.ts b/src/modules/user/repository/repositories/user-password-history.repository.ts deleted file mode 100644 index 435d039d1..000000000 --- a/src/modules/user/repository/repositories/user-password-history.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; -import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; -import { - UserPasswordHistoryDoc, - UserPasswordHistoryEntity, -} from 'src/modules/user/repository/entities/user-password-history.entity'; - -@Injectable() -export class UserPasswordHistoryRepository extends DatabaseMongoUUIDRepositoryAbstract< - UserPasswordHistoryEntity, - UserPasswordHistoryDoc -> { - constructor( - @DatabaseModel(UserPasswordHistoryEntity.name) - private readonly userPasswordHistoryModel: Model - ) { - super(userPasswordHistoryModel); - } -} diff --git a/src/modules/user/repository/repositories/user-state-history.repository.ts b/src/modules/user/repository/repositories/user-state-history.repository.ts deleted file mode 100644 index a15743ba1..000000000 --- a/src/modules/user/repository/repositories/user-state-history.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; -import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; -import { - UserStateHistoryDoc, - UserStateHistoryEntity, -} from 'src/modules/user/repository/entities/user-state-history.entity'; - -@Injectable() -export class UserStateHistoryRepository extends DatabaseMongoUUIDRepositoryAbstract< - UserStateHistoryEntity, - UserStateHistoryDoc -> { - constructor( - @DatabaseModel(UserStateHistoryEntity.name) - private readonly userStateHistoryModel: Model - ) { - super(userStateHistoryModel); - } -} diff --git a/src/modules/user/repository/repositories/user.repository.ts b/src/modules/user/repository/repositories/user.repository.ts index ca88faf72..d4e2291ae 100644 --- a/src/modules/user/repository/repositories/user.repository.ts +++ b/src/modules/user/repository/repositories/user.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Model } from 'mongoose'; -import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { Model, PopulateOptions } from 'mongoose'; +import { DatabaseRepositoryAbstract } from 'src/common/database/abstracts/database.repository.abstract'; import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; import { CountryEntity } from 'src/modules/country/repository/entities/country.entity'; import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; @@ -10,33 +10,60 @@ import { } from 'src/modules/user/repository/entities/user.entity'; @Injectable() -export class UserRepository extends DatabaseMongoUUIDRepositoryAbstract< +export class UserRepository extends DatabaseRepositoryAbstract< UserEntity, UserDoc > { + readonly _joinActive: PopulateOptions[] = [ + { + path: 'role', + localField: 'role', + foreignField: '_id', + model: RoleEntity.name, + justOne: true, + match: { + isActive: true, + }, + }, + { + path: 'country', + localField: 'country', + foreignField: '_id', + model: CountryEntity.name, + justOne: true, + }, + { + path: 'mobileNumber.country', + localField: 'mobileNumber.country', + foreignField: '_id', + model: CountryEntity.name, + justOne: true, + }, + ]; + constructor( @DatabaseModel(UserEntity.name) private readonly userModel: Model ) { super(userModel, [ { - field: 'role', - localKey: 'role', - foreignKey: '_id', + path: 'role', + localField: 'role', + foreignField: '_id', model: RoleEntity.name, justOne: true, }, { - field: 'country', - localKey: 'country', - foreignKey: '_id', + path: 'country', + localField: 'country', + foreignField: '_id', model: CountryEntity.name, justOne: true, }, { - field: 'mobileNumber.country', - localKey: 'mobileNumber.country', - foreignKey: '_id', + path: 'mobileNumber.country', + localField: 'mobileNumber.country', + foreignField: '_id', model: CountryEntity.name, justOne: true, }, diff --git a/src/modules/user/repository/user.repository.module.ts b/src/modules/user/repository/user.repository.module.ts index 8a0ba9856..739abcc77 100644 --- a/src/modules/user/repository/user.repository.module.ts +++ b/src/modules/user/repository/user.repository.module.ts @@ -1,40 +1,15 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; -import { - UserLoginHistoryEntity, - UserLoginHistorySchema, -} from 'src/modules/user/repository/entities/user-login-history.entity'; -import { - UserPasswordHistoryEntity, - UserPasswordHistorySchema, -} from 'src/modules/user/repository/entities/user-password-history.entity'; -import { - UserStateHistoryEntity, - UserStateHistorySchema, -} from 'src/modules/user/repository/entities/user-state-history.entity'; import { UserEntity, UserSchema, } from 'src/modules/user/repository/entities/user.entity'; -import { UserLoginHistoryRepository } from 'src/modules/user/repository/repositories/user-login-history.repository'; -import { UserPasswordHistoryRepository } from 'src/modules/user/repository/repositories/user-password-history.repository'; -import { UserStateHistoryRepository } from 'src/modules/user/repository/repositories/user-state-history.repository'; import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; @Module({ - providers: [ - UserRepository, - UserStateHistoryRepository, - UserPasswordHistoryRepository, - UserLoginHistoryRepository, - ], - exports: [ - UserRepository, - UserStateHistoryRepository, - UserPasswordHistoryRepository, - UserLoginHistoryRepository, - ], + providers: [UserRepository], + exports: [UserRepository], controllers: [], imports: [ MongooseModule.forFeature( @@ -43,18 +18,6 @@ import { UserRepository } from 'src/modules/user/repository/repositories/user.re name: UserEntity.name, schema: UserSchema, }, - { - name: UserStateHistoryEntity.name, - schema: UserStateHistorySchema, - }, - { - name: UserLoginHistoryEntity.name, - schema: UserLoginHistorySchema, - }, - { - name: UserPasswordHistoryEntity.name, - schema: UserPasswordHistorySchema, - }, ], DATABASE_CONNECTION_NAME ), diff --git a/src/modules/user/services/user-login-history.service.ts b/src/modules/user/services/user-login-history.service.ts deleted file mode 100644 index 7dcb2c667..000000000 --- a/src/modules/user/services/user-login-history.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { Request } from 'express'; -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { UserLoginHistoryCreateRequest } from 'src/modules/user/dtos/request/user-login-history.create.request.dto'; -import { UserLoginHistoryListResponseDto } from 'src/modules/user/dtos/response/user-login-history.list.response.dto'; -import { IUserLoginHistoryService } from 'src/modules/user/interfaces/user-login-history.service.interface'; -import { - UserLoginHistoryDoc, - UserLoginHistoryEntity, -} from 'src/modules/user/repository/entities/user-login-history.entity'; -import { UserLoginHistoryRepository } from 'src/modules/user/repository/repositories/user-login-history.repository'; - -@Injectable() -export class UserLoginHistoryService implements IUserLoginHistoryService { - constructor( - private readonly userLoginHistoryRepository: UserLoginHistoryRepository - ) {} - - async findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userLoginHistoryRepository.findAll( - find, - options - ); - } - - async findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userLoginHistoryRepository.findAll( - { user, ...find }, - options - ); - } - - async findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userLoginHistoryRepository.findOneById( - _id, - options - ); - } - - async findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userLoginHistoryRepository.findOne( - find, - options - ); - } - - async getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userLoginHistoryRepository.getTotal(find, options); - } - - async getTotalByUser( - user: string, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userLoginHistoryRepository.getTotal({ user }, options); - } - - async create( - request: Request, - { user }: UserLoginHistoryCreateRequest, - options?: IDatabaseCreateOptions - ): Promise { - const create = new UserLoginHistoryEntity(); - create.user = user; - create.hostname = request.hostname; - create.ip = request.ip; - create.protocol = request.protocol; - create.originalUrl = request.originalUrl; - create.method = request.method; - - create.userAgent = request.headers['user-agent'] as string; - create.xForwardedFor = request.headers['x-forwarded-for'] as string; - create.xForwardedHost = request.headers['x-forwarded-host'] as string; - create.xForwardedPorto = request.headers['x-forwarded-porto'] as string; - - return this.userLoginHistoryRepository.create( - create, - options - ); - } - - async mapList( - userHistories: UserLoginHistoryDoc[] - ): Promise { - return plainToInstance(UserLoginHistoryListResponseDto, userHistories); - } -} diff --git a/src/modules/user/services/user-password-history.service.ts b/src/modules/user/services/user-password-history.service.ts deleted file mode 100644 index 88063b65d..000000000 --- a/src/modules/user/services/user-password-history.service.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { plainToInstance } from 'class-transformer'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { UserPasswordHistoryListResponseDto } from 'src/modules/user/dtos/response/user-password-history.list.response.dto'; -import { IUserPasswordHistoryService } from 'src/modules/user/interfaces/user-password-history.service.interface'; -import { - UserPasswordHistoryDoc, - UserPasswordHistoryEntity, -} from 'src/modules/user/repository/entities/user-password-history.entity'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; -import { UserPasswordHistoryRepository } from 'src/modules/user/repository/repositories/user-password-history.repository'; - -@Injectable() -export class UserPasswordHistoryService implements IUserPasswordHistoryService { - private readonly passwordPeriod: number; - - constructor( - private readonly configService: ConfigService, - private readonly helperDateService: HelperDateService, - private readonly userPasswordHistoryRepository: UserPasswordHistoryRepository - ) { - this.passwordPeriod = this.configService.get( - 'auth.password.period' - ); - } - - async findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userPasswordHistoryRepository.findAll( - find, - options - ); - } - - async findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userPasswordHistoryRepository.findAll( - { ...find, user }, - options - ); - } - - async findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userPasswordHistoryRepository.findOneById( - _id, - options - ); - } - - async findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userPasswordHistoryRepository.findOne( - find, - options - ); - } - - async findOneByUser( - user: UserDoc, - password: IAuthPassword, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userPasswordHistoryRepository.findOne( - { - user: user._id, - password: password.passwordHash, - }, - options - ); - } - - async getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userPasswordHistoryRepository.getTotal(find, options); - } - - async getTotalByUser( - user: string, - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userPasswordHistoryRepository.getTotal( - { ...find, user }, - options - ); - } - - async createByUser( - user: UserDoc, - options?: IDatabaseCreateOptions - ): Promise { - const create: UserPasswordHistoryEntity = - new UserPasswordHistoryEntity(); - create.user = user._id; - create.by = user._id; - create.password = user.password; - - return this.userPasswordHistoryRepository.create( - create, - options - ); - } - - async createByAdmin( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const create: UserPasswordHistoryEntity = - new UserPasswordHistoryEntity(); - create.user = user._id; - create.by = by; - create.password = user.password; - - return this.userPasswordHistoryRepository.create( - create, - options - ); - } - - async mapList( - userHistories: UserPasswordHistoryDoc[] - ): Promise { - return plainToInstance( - UserPasswordHistoryListResponseDto, - userHistories - ); - } - async checkPasswordPeriodByUser( - user: UserDoc, - password: IAuthPassword, - options?: IDatabaseFindOneOptions - ): Promise { - const pass = - await this.userPasswordHistoryRepository.findOne( - { - user: user._id, - password: password.passwordHash, - }, - options - ); - - const today: Date = this.helperDateService.create(); - const passwordPeriod: Date = this.helperDateService.forwardInSeconds( - this.passwordPeriod, - { fromDate: pass.createdAt } - ); - - return today > passwordPeriod; - } - - async getPasswordPeriod(): Promise { - return this.passwordPeriod; - } -} diff --git a/src/modules/user/services/user-state-history.service.ts b/src/modules/user/services/user-state-history.service.ts deleted file mode 100644 index 3d9f03c10..000000000 --- a/src/modules/user/services/user-state-history.service.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { - IDatabaseCreateOptions, - IDatabaseFindAllOptions, - IDatabaseFindOneOptions, - IDatabaseGetTotalOptions, -} from 'src/common/database/interfaces/database.interface'; -import { ENUM_USER_HISTORY_STATE } from 'src/modules/user/constants/user-history.enum.constant'; -import { ENUM_USER_STATUS } from 'src/modules/user/constants/user.enum.constant'; -import { UserStateHistoryListResponseDto } from 'src/modules/user/dtos/response/user-state-history.list.response.dto'; -import { IUserStateHistoryService } from 'src/modules/user/interfaces/user-state-history.service.interface'; -import { - UserStateHistoryDoc, - UserStateHistoryEntity, -} from 'src/modules/user/repository/entities/user-state-history.entity'; -import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; -import { UserStateHistoryRepository } from 'src/modules/user/repository/repositories/user-state-history.repository'; - -@Injectable() -export class UserStateHistoryService implements IUserStateHistoryService { - constructor( - private readonly userStateHistoryRepository: UserStateHistoryRepository - ) {} - - async findAll( - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userStateHistoryRepository.findAll( - find, - options - ); - } - - async findAllByUser( - user: string, - find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userStateHistoryRepository.findAll( - { ...find, user }, - options - ); - } - - async findOneById( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userStateHistoryRepository.findOneById( - _id, - options - ); - } - - async findOne( - find: Record, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userStateHistoryRepository.findOne( - find, - options - ); - } - - async getTotal( - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userStateHistoryRepository.getTotal(find, options); - } - - async getTotalByUser( - user: string, - find?: Record, - options?: IDatabaseGetTotalOptions - ): Promise { - return this.userStateHistoryRepository.getTotal( - { ...find, user }, - options - ); - } - - async setState(user: UserDoc): Promise { - if (user.blocked) { - return ENUM_USER_HISTORY_STATE.BLOCKED; - } - - switch (user.status) { - case ENUM_USER_STATUS.DELETED: - return ENUM_USER_HISTORY_STATE.DELETED; - case ENUM_USER_STATUS.ACTIVE: - return ENUM_USER_HISTORY_STATE.ACTIVE; - case ENUM_USER_STATUS.INACTIVE: - default: - return ENUM_USER_HISTORY_STATE.INACTIVE; - } - } - - async createCreated( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const create: UserStateHistoryEntity = new UserStateHistoryEntity(); - create.afterState = ENUM_USER_HISTORY_STATE.ACTIVE; - create.beforeState = ENUM_USER_HISTORY_STATE.CREATED; - create.user = user._id; - create.by = by; - - return this.userStateHistoryRepository.create( - create, - options - ); - } - - async createActive( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const beforeState = await this.setState(user); - const create: UserStateHistoryEntity = new UserStateHistoryEntity(); - create.afterState = ENUM_USER_HISTORY_STATE.ACTIVE; - create.beforeState = beforeState; - create.user = user._id; - create.by = by; - - return this.userStateHistoryRepository.create( - create, - options - ); - } - - async createInactive( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const beforeState = await this.setState(user); - const create: UserStateHistoryEntity = new UserStateHistoryEntity(); - create.afterState = ENUM_USER_HISTORY_STATE.INACTIVE; - create.beforeState = beforeState; - create.user = user._id; - create.by = by; - - return this.userStateHistoryRepository.create( - create, - options - ); - } - - async createBlocked( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const beforeState = await this.setState(user); - const create: UserStateHistoryEntity = new UserStateHistoryEntity(); - create.afterState = ENUM_USER_HISTORY_STATE.BLOCKED; - create.beforeState = beforeState; - create.user = user._id; - create.by = by; - - return this.userStateHistoryRepository.create( - create, - options - ); - } - - async createDeleted( - user: UserDoc, - by: string, - options?: IDatabaseCreateOptions - ): Promise { - const beforeState = await this.setState(user); - const create: UserStateHistoryEntity = new UserStateHistoryEntity(); - create.afterState = ENUM_USER_HISTORY_STATE.DELETED; - create.beforeState = beforeState; - create.user = user._id; - create.by = by; - - return this.userStateHistoryRepository.create( - create, - options - ); - } - - async mapList( - userHistories: UserStateHistoryDoc[] - ): Promise { - return plainToInstance(UserStateHistoryListResponseDto, userHistories); - } -} diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index e1b58bff5..98131afbf 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -1,49 +1,67 @@ import { Injectable } from '@nestjs/common'; -import { IUserService } from 'src/modules/user/interfaces/user.service.interface'; import { IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, IDatabaseExistOptions, IDatabaseFindAllOptions, - IDatabaseFindOneOptions, IDatabaseGetTotalOptions, - IDatabaseManyOptions, + IDatabaseOptions, IDatabaseSaveOptions, + IDatabaseUpdateOptions, } from 'src/common/database/interfaces/database.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ConfigService } from '@nestjs/config'; +import { IAuthPassword } from 'src/modules/auth/interfaces/auth.interface'; +import { plainToInstance } from 'class-transformer'; +import { Document } from 'mongoose'; +import { DatabaseQueryContain } from 'src/common/database/decorators/database.decorator'; +import { IUserService } from 'src/modules/user/interfaces/user.service.interface'; +import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; import { UserDoc, UserEntity, } from 'src/modules/user/repository/entities/user.entity'; -import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; -import { HelperDateService } from 'src/common/helper/services/helper.date.service'; -import { ConfigService } from '@nestjs/config'; -import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; -import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; -import { plainToInstance } from 'class-transformer'; -import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; +import { + IUserDoc, + IUserEntity, +} from 'src/modules/user/interfaces/user.interface'; import { ENUM_USER_SIGN_UP_FROM, ENUM_USER_STATUS, -} from 'src/modules/user/constants/user.enum.constant'; +} from 'src/modules/user/enums/user.enum'; import { UserCreateRequestDto } from 'src/modules/user/dtos/request/user.create.request.dto'; -import { AwsS3Dto } from 'src/common/aws/dtos/aws.s3.dto'; import { UserUpdatePasswordAttemptRequestDto } from 'src/modules/user/dtos/request/user.update-password-attempt.request.dto'; -import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.request.dto'; -import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; -import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; -import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; -import { UserSignUpRequestDto } from 'src/modules/user/dtos/request/user.sign-up.request.dto'; +import { UserUpdateRequestDto } from 'src/modules/user/dtos/request/user.update.request.dto'; import { UserUpdateMobileNumberRequestDto } from 'src/modules/user/dtos/request/user.update-mobile-number.request.dto'; -import { CountryEntity } from 'src/modules/country/repository/entities/country.entity'; +import { UserProfileResponseDto } from 'src/modules/user/dtos/response/user.profile.response.dto'; +import { UserListResponseDto } from 'src/modules/user/dtos/response/user.list.response.dto'; +import { UserShortResponseDto } from 'src/modules/user/dtos/response/user.short.response.dto'; +import { UserGetResponseDto } from 'src/modules/user/dtos/response/user.get.response.dto'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { AuthSignUpRequestDto } from 'src/modules/auth/dtos/request/auth.sign-up.request.dto'; +import { UserUpdateClaimUsernameRequestDto } from 'src/modules/user/dtos/request/user.update-claim-username.dto'; +import { DatabaseSoftDeleteDto } from 'src/common/database/dtos/database.soft-delete.dto'; +import { UserUpdateProfileRequestDto } from 'src/modules/user/dtos/request/user.update-profile.dto'; @Injectable() export class UserService implements IUserService { + private readonly usernamePrefix: string; + private readonly usernamePattern: RegExp; private readonly uploadPath: string; constructor( private readonly userRepository: UserRepository, private readonly helperDateService: HelperDateService, - private readonly configService: ConfigService + private readonly configService: ConfigService, + private readonly helperStringService: HelperStringService ) { + this.usernamePrefix = this.configService.get( + 'user.usernamePrefix' + ); + this.usernamePattern = this.configService.get( + 'user.usernamePattern' + ); this.uploadPath = this.configService.get('user.uploadPath'); } @@ -54,49 +72,137 @@ export class UserService implements IUserService { return this.userRepository.findAll(find, options); } - async findAllWithRoleAndCountry( + async getTotal( find?: Record, - options?: IDatabaseFindAllOptions - ): Promise { - return this.userRepository.findAll(find, { - ...options, - join: true, - }); + options?: IDatabaseGetTotalOptions + ): Promise { + return this.userRepository.getTotal(find, options); } async findOneById( _id: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.userRepository.findOneById(_id, options); } async findOne( find: Record, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.userRepository.findOne(find, options); } async findOneByEmail( email: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.userRepository.findOne({ email }, options); } async findOneByMobileNumber( mobileNumber: string, - options?: IDatabaseFindOneOptions + options?: IDatabaseOptions ): Promise { return this.userRepository.findOne({ mobileNumber }, options); } - async getTotal( + async findAllWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findAll(find, { + ...options, + join: true, + }); + } + + async findOneWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findOne(find, { + ...options, + join: true, + }); + } + + async findOneWithRoleAndCountryById( + _id: string, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findOneById(_id, { + ...options, + join: true, + }); + } + + async findAllActiveWithRoleAndCountry( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findAll( + { ...find, status: ENUM_USER_STATUS.ACTIVE }, + { + ...options, + join: this.userRepository._joinActive, + } + ); + } + + async getTotalActive( find?: Record, options?: IDatabaseGetTotalOptions ): Promise { - return this.userRepository.getTotal(find, options); + return this.userRepository.getTotal( + { ...find, status: ENUM_USER_STATUS.ACTIVE }, + { + ...options, + join: this.userRepository._joinActive, + } + ); + } + + async findOneActiveById( + _id: string, + options?: IDatabaseOptions + ): Promise { + return this.userRepository.findOne( + { _id, status: ENUM_USER_STATUS.ACTIVE }, + { + ...options, + join: this.userRepository._joinActive, + } + ); + } + + async findOneActiveByEmail( + email: string, + options?: IDatabaseOptions + ): Promise { + return this.userRepository.findOne( + { email, status: ENUM_USER_STATUS.ACTIVE }, + { + ...options, + join: this.userRepository._joinActive, + } + ); + } + + async findOneActiveByMobileNumber( + mobileNumber: string, + options?: IDatabaseOptions + ): Promise { + return this.userRepository.findOne( + { + mobileNumber, + status: ENUM_USER_STATUS.ACTIVE, + }, + { + ...options, + join: this.userRepository._joinActive, + } + ); } async create( @@ -105,12 +211,13 @@ export class UserService implements IUserService { signUpFrom: ENUM_USER_SIGN_UP_FROM, options?: IDatabaseCreateOptions ): Promise { + const username = await this.createRandomUsername(); + const create: UserEntity = new UserEntity(); create.name = name; create.email = email; create.role = role; create.status = ENUM_USER_STATUS.ACTIVE; - create.blocked = false; create.password = passwordHash; create.salt = salt; create.passwordExpired = passwordExpired; @@ -119,22 +226,24 @@ export class UserService implements IUserService { create.signUpDate = this.helperDateService.create(); create.signUpFrom = signUpFrom; create.country = country; + create.username = username; return this.userRepository.create(create, options); } async signUp( role: string, - { email, name, country }: UserSignUpRequestDto, + { email, name, country }: AuthSignUpRequestDto, { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, options?: IDatabaseCreateOptions ): Promise { + const username = await this.createRandomUsername(); + const create: UserEntity = new UserEntity(); create.name = name; create.email = email; create.role = role; create.status = ENUM_USER_STATUS.ACTIVE; - create.blocked = false; create.password = passwordHash; create.salt = salt; create.passwordExpired = passwordExpired; @@ -143,6 +252,7 @@ export class UserService implements IUserService { create.signUpDate = this.helperDateService.create(); create.signUpFrom = ENUM_USER_SIGN_UP_FROM.PUBLIC; create.country = country; + create.username = username; return this.userRepository.create(create, options); } @@ -152,12 +262,17 @@ export class UserService implements IUserService { options?: IDatabaseExistOptions ): Promise { return this.userRepository.exists( - { - email: { - $regex: new RegExp(`\\b${email}\\b`), - $options: 'i', - }, - }, + DatabaseQueryContain('email', email, { fullWord: true }), + { ...options, withDeleted: true } + ); + } + + async existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + DatabaseQueryContain('username', username, { fullWord: true }), { ...options, withDeleted: true } ); } @@ -215,273 +330,194 @@ export class UserService implements IUserService { return this.userRepository.save(repository, options); } - async selfDelete( + async blocked( repository: UserDoc, options?: IDatabaseSaveOptions ): Promise { - repository.status = ENUM_USER_STATUS.DELETED; - repository.selfDeletion = true; + repository.status = ENUM_USER_STATUS.BLOCKED; return this.userRepository.save(repository, options); } - async blocked( + async updatePasswordAttempt( repository: UserDoc, + { passwordAttempt }: UserUpdatePasswordAttemptRequestDto, options?: IDatabaseSaveOptions ): Promise { - repository.blocked = true; + repository.passwordAttempt = passwordAttempt; return this.userRepository.save(repository, options); } - async unblocked( + async increasePasswordAttempt( + repository: UserDoc, + options?: IDatabaseUpdateOptions + ): Promise { + return this.userRepository.update( + { _id: repository._id }, + { + $inc: { + passwordAttempt: 1, + }, + }, + options + ); + } + + async resetPasswordAttempt( repository: UserDoc, options?: IDatabaseSaveOptions ): Promise { - repository.blocked = false; + repository.passwordAttempt = 0; return this.userRepository.save(repository, options); } - async updatePasswordAttempt( + async updatePasswordExpired( repository: UserDoc, - { passwordAttempt }: UserUpdatePasswordAttemptRequestDto, + passwordExpired: Date, options?: IDatabaseSaveOptions ): Promise { - repository.passwordAttempt = passwordAttempt; + repository.passwordExpired = passwordExpired; return this.userRepository.save(repository, options); } - async increasePasswordAttempt( + async update( repository: UserDoc, + { country, name, role }: UserUpdateRequestDto, options?: IDatabaseSaveOptions ): Promise { - repository.passwordAttempt = ++repository.passwordAttempt; + repository.country = country; + repository.name = name; + repository.role = role; return this.userRepository.save(repository, options); } - async resetPasswordAttempt( + async updateMobileNumber( repository: UserDoc, + { country, number }: UserUpdateMobileNumberRequestDto, options?: IDatabaseSaveOptions ): Promise { - repository.passwordAttempt = 0; + repository.mobileNumber = { + country, + number, + }; return this.userRepository.save(repository, options); } - async updatePasswordExpired( + async updateClaimUsername( repository: UserDoc, - passwordExpired: Date, + { username }: UserUpdateClaimUsernameRequestDto, options?: IDatabaseSaveOptions ): Promise { - repository.passwordExpired = passwordExpired; + repository.username = username; return this.userRepository.save(repository, options); } - async join(repository: UserDoc): Promise { - return this.userRepository.join(repository, [ - { - field: 'role', - localKey: 'role', - foreignKey: '_id', - model: RoleEntity.name, - justOne: true, - }, - { - field: 'country', - localKey: 'country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - { - field: 'mobileNumber.country', - localKey: 'mobileNumber.country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - ]); + async removeMobileNumber( + repository: UserDoc, + options?: IDatabaseSaveOptions + ): Promise { + repository.mobileNumber = undefined; + + return this.userRepository.save(repository, options); } - async getPhotoUploadPath(user: string): Promise { - return this.uploadPath.replace('{user}', user); + async delete( + repository: UserDoc, + dto: DatabaseSoftDeleteDto, + options?: IDatabaseSaveOptions + ): Promise { + return this.userRepository.softDelete(repository, dto, options); } async deleteMany( find: Record, - options?: IDatabaseManyOptions + options?: IDatabaseDeleteManyOptions ): Promise { - return this.userRepository.deleteMany(find, options); - } + try { + await this.userRepository.deleteMany(find, options); - async findOneByIdAndActive( - _id: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userRepository.findOne( - { _id, status: ENUM_USER_STATUS.ACTIVE, blocked: false }, - { - ...options, - join: [ - { - field: 'role', - localKey: 'role', - foreignKey: '_id', - model: RoleEntity.name, - justOne: true, - condition: { - isActive: true, - }, - }, - { - field: 'country', - localKey: 'country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - { - field: 'mobileNumber.country', - localKey: 'mobileNumber.country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - ], - } - ); - } - - async findOneByEmailAndActive( - email: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userRepository.findOne( - { email, status: ENUM_USER_STATUS.ACTIVE, blocked: false }, - { - ...options, - join: [ - { - field: 'role', - localKey: 'role', - foreignKey: '_id', - model: RoleEntity.name, - justOne: true, - condition: { - isActive: true, - }, - }, - { - field: 'country', - localKey: 'country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - { - field: 'mobileNumber.country', - localKey: 'mobileNumber.country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - ], - } - ); - } - - async findOneByMobileNumberAndActive( - mobileNumber: string, - options?: IDatabaseFindOneOptions - ): Promise { - return this.userRepository.findOne( - { - mobileNumber, - status: ENUM_USER_STATUS.ACTIVE, - blocked: false, - }, - { - ...options, - join: [ - { - field: 'role', - localKey: 'role', - foreignKey: '_id', - model: RoleEntity.name, - justOne: true, - condition: { - isActive: true, - }, - }, - { - field: 'country', - localKey: 'country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - { - field: 'mobileNumber.country', - localKey: 'mobileNumber.country', - foreignKey: '_id', - model: CountryEntity.name, - justOne: true, - }, - ], - } - ); - } - - async mapProfile(user: IUserDoc): Promise { - return plainToInstance(UserProfileResponseDto, user.toObject()); + return true; + } catch (error: unknown) { + throw error; + } } async updateProfile( repository: UserDoc, - { name, familyName, address }: UserUpdateProfileRequestDto, + { country, name, address, familyName }: UserUpdateProfileRequestDto, options?: IDatabaseSaveOptions ): Promise { + repository.country = country; repository.name = name; - repository.familyName = familyName; repository.address = address; + repository.familyName = familyName; return this.userRepository.save(repository, options); } - async updateMobileNumber( - repository: UserDoc, - { country, number }: UserUpdateMobileNumberRequestDto, - options?: IDatabaseSaveOptions - ): Promise { - repository.mobileNumber = { - country, - number, - }; + async join(repository: UserDoc): Promise { + return this.userRepository.join(repository, this.userRepository._join); + } - return this.userRepository.save(repository, options); + async getPhotoUploadPath(user: string): Promise { + return this.uploadPath.replace('{user}', user); } - async deleteMobileNumber( - repository: UserDoc, - options?: IDatabaseSaveOptions - ): Promise { - repository.mobileNumber = undefined; + async createRandomFilenamePhoto(): Promise { + return this.helperStringService.random(10); + } - return this.userRepository.save(repository, options); + async createRandomUsername(): Promise { + const suffix = this.helperStringService.random(6); + + return `${this.usernamePrefix}-${suffix}`; + } + + async checkUsername(username: string): Promise { + return username.search(this.usernamePattern) === -1; + } + + async mapProfile( + user: IUserDoc | IUserEntity + ): Promise { + return plainToInstance( + UserProfileResponseDto, + user instanceof Document ? user.toObject() : user + ); } - async mapList(user: IUserDoc[]): Promise { + async mapList( + users: IUserDoc[] | IUserEntity[] + ): Promise { return plainToInstance( UserListResponseDto, - user.map(u => u.toObject()) + users.map((u: IUserDoc | IUserEntity) => + u instanceof Document ? u.toObject() : u + ) ); } - async mapGet(user: IUserDoc): Promise { - return plainToInstance(UserGetResponseDto, user.toObject()); + async mapShort( + users: IUserDoc[] | IUserEntity[] + ): Promise { + return plainToInstance( + UserShortResponseDto, + users.map((u: IUserDoc | IUserEntity) => + u instanceof Document ? u.toObject() : u + ) + ); + } + + async mapGet(user: IUserDoc | IUserEntity): Promise { + return plainToInstance( + UserGetResponseDto, + user instanceof Document ? user.toObject() : user + ); } } diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 1cb718403..8f900389c 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,24 +1,11 @@ import { Module } from '@nestjs/common'; -import { UserRepositoryModule } from 'src/modules/user/repository/user.repository.module'; import { UserService } from './services/user.service'; -import { UserLoginHistoryService } from 'src/modules/user/services/user-login-history.service'; -import { UserStateHistoryService } from 'src/modules/user/services/user-state-history.service'; -import { UserPasswordHistoryService } from 'src/modules/user/services/user-password-history.service'; +import { UserRepositoryModule } from 'src/modules/user/repository/user.repository.module'; @Module({ imports: [UserRepositoryModule], - exports: [ - UserService, - UserStateHistoryService, - UserPasswordHistoryService, - UserLoginHistoryService, - ], - providers: [ - UserService, - UserStateHistoryService, - UserPasswordHistoryService, - UserLoginHistoryService, - ], + exports: [UserService], + providers: [UserService], controllers: [], }) export class UserModule {} diff --git a/src/modules/verification/verification.module.ts b/src/modules/verification/verification.module.ts new file mode 100644 index 000000000..4a4281736 --- /dev/null +++ b/src/modules/verification/verification.module.ts @@ -0,0 +1 @@ +// TODO: VERIFICATION EMAIL MODULE diff --git a/src/router/router.module.ts b/src/router/router.module.ts index e98e4011b..256f6377b 100644 --- a/src/router/router.module.ts +++ b/src/router/router.module.ts @@ -2,9 +2,9 @@ import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; import { RouterModule as NestJsRouterModule } from '@nestjs/core'; import { RoutesUserModule } from 'src/router/routes/routes.user.module'; import { RoutesPublicModule } from 'src/router/routes/routes.public.module'; -import { RoutesPrivateModule } from 'src/router/routes/routes.private.module'; import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; -import { RoutesAuthModule } from 'src/router/routes/routes.auth.module'; +import { RoutesSystemModule } from 'src/router/routes/routes.system.module'; +import { RoutesSharedModule } from 'src/router/routes/routes.shared.module'; @Module({}) export class RouterModule { @@ -19,18 +19,18 @@ export class RouterModule { if (process.env.HTTP_ENABLE === 'true') { imports.push( RoutesPublicModule, - RoutesPrivateModule, + RoutesSystemModule, RoutesUserModule, RoutesAdminModule, - RoutesAuthModule, + RoutesSharedModule, NestJsRouterModule.register([ { path: '/public', module: RoutesPublicModule, }, { - path: '/private', - module: RoutesPrivateModule, + path: '/system', + module: RoutesSystemModule, }, { path: '/admin', @@ -41,8 +41,8 @@ export class RouterModule { module: RoutesUserModule, }, { - path: '/auth', - module: RoutesAuthModule, + path: '/shared', + module: RoutesSharedModule, }, ]) ); diff --git a/src/router/routes/routes.admin.module.ts b/src/router/routes/routes.admin.module.ts index 91930f528..d59418afc 100644 --- a/src/router/routes/routes.admin.module.ts +++ b/src/router/routes/routes.admin.module.ts @@ -1,8 +1,9 @@ +import { BullModule } from '@nestjs/bullmq'; import { Module } from '@nestjs/common'; -import { ApiKeyModule } from 'src/common/api-key/api-key.module'; -import { ApiKeyAdminController } from 'src/common/api-key/controllers/api-key.admin.controller'; -import { AuthModule } from 'src/common/auth/auth.module'; -import { PaginationModule } from 'src/common/pagination/pagination.module'; +import { ApiKeyModule } from 'src/modules/api-key/api-key.module'; +import { ApiKeyAdminController } from 'src/modules/api-key/controllers/api-key.admin.controller'; +import { AuthModule } from 'src/modules/auth/auth.module'; +import { AuthAdminController } from 'src/modules/auth/controllers/auth.admin.controller'; import { CountryAdminController } from 'src/modules/country/controllers/country.admin.controller'; import { CountryModule } from 'src/modules/country/country.module'; import { EmailModule } from 'src/modules/email/email.module'; @@ -12,6 +13,9 @@ import { SettingAdminController } from 'src/modules/setting/controllers/setting. import { SettingModule } from 'src/modules/setting/setting.module'; import { UserAdminController } from 'src/modules/user/controllers/user.admin.controller'; import { UserModule } from 'src/modules/user/user.module'; +import { WORKER_CONNECTION_NAME } from 'src/worker/constants/worker.constant'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; + @Module({ controllers: [ ApiKeyAdminController, @@ -19,18 +23,24 @@ import { UserModule } from 'src/modules/user/user.module'; RoleAdminController, UserAdminController, CountryAdminController, + AuthAdminController, ], providers: [], exports: [], imports: [ ApiKeyModule, - PaginationModule, SettingModule, RoleModule, UserModule, AuthModule, EmailModule, CountryModule, + BullModule.registerQueue({ + connection: { + name: WORKER_CONNECTION_NAME, + }, + name: ENUM_WORKER_QUEUES.EMAIL_QUEUE, + }), ], }) export class RoutesAdminModule {} diff --git a/src/router/routes/routes.auth.module.ts b/src/router/routes/routes.auth.module.ts deleted file mode 100644 index 3be139d27..000000000 --- a/src/router/routes/routes.auth.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthModule } from 'src/common/auth/auth.module'; -import { AwsModule } from 'src/common/aws/aws.module'; -import { CountryModule } from 'src/modules/country/country.module'; -import { SettingModule } from 'src/modules/setting/setting.module'; -import { UserAuthController } from 'src/modules/user/controllers/user.auth.controller'; -import { UserModule } from 'src/modules/user/user.module'; - -@Module({ - controllers: [UserAuthController], - providers: [], - exports: [], - imports: [UserModule, AuthModule, AwsModule, SettingModule, CountryModule], -}) -export class RoutesAuthModule {} diff --git a/src/router/routes/routes.private.module.ts b/src/router/routes/routes.private.module.ts deleted file mode 100644 index 6ac7ec897..000000000 --- a/src/router/routes/routes.private.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TerminusModule } from '@nestjs/terminus'; -import { PaginationModule } from 'src/common/pagination/pagination.module'; -import { CountryPrivateController } from 'src/modules/country/controllers/country.private.controller'; -import { CountryModule } from 'src/modules/country/country.module'; -import { HealthPrivateController } from 'src/modules/health/controllers/health.private.controller'; -import { HealthModule } from 'src/modules/health/health.module'; -import { SettingPrivateController } from 'src/modules/setting/controllers/setting.private.controller'; -import { SettingModule } from 'src/modules/setting/setting.module'; - -@Module({ - controllers: [ - HealthPrivateController, - SettingPrivateController, - CountryPrivateController, - ], - providers: [], - exports: [], - imports: [ - HealthModule, - TerminusModule, - PaginationModule, - SettingModule, - CountryModule, - ], -}) -export class RoutesPrivateModule {} diff --git a/src/router/routes/routes.public.module.ts b/src/router/routes/routes.public.module.ts index b599f244a..beefe783c 100644 --- a/src/router/routes/routes.public.module.ts +++ b/src/router/routes/routes.public.module.ts @@ -1,15 +1,18 @@ +import { BullModule } from '@nestjs/bullmq'; import { Module } from '@nestjs/common'; -import { AuthModule } from 'src/common/auth/auth.module'; +import { AuthModule } from 'src/modules/auth/auth.module'; +import { AuthPublicController } from 'src/modules/auth/controllers/auth.public.controller'; import { CountryModule } from 'src/modules/country/country.module'; import { EmailModule } from 'src/modules/email/email.module'; import { HelloPublicController } from 'src/modules/hello/controllers/hello.public.controller'; import { RoleModule } from 'src/modules/role/role.module'; import { SettingModule } from 'src/modules/setting/setting.module'; -import { UserPublicController } from 'src/modules/user/controllers/user.public.controller'; import { UserModule } from 'src/modules/user/user.module'; +import { WORKER_CONNECTION_NAME } from 'src/worker/constants/worker.constant'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; @Module({ - controllers: [HelloPublicController, UserPublicController], + controllers: [HelloPublicController, AuthPublicController], providers: [], exports: [], imports: [ @@ -19,6 +22,12 @@ import { UserModule } from 'src/modules/user/user.module'; RoleModule, EmailModule, CountryModule, + BullModule.registerQueue({ + connection: { + name: WORKER_CONNECTION_NAME, + }, + name: ENUM_WORKER_QUEUES.EMAIL_QUEUE, + }), ], }) export class RoutesPublicModule {} diff --git a/src/router/routes/routes.shared.module.ts b/src/router/routes/routes.shared.module.ts new file mode 100644 index 000000000..3e9340662 --- /dev/null +++ b/src/router/routes/routes.shared.module.ts @@ -0,0 +1,31 @@ +import { BullModule } from '@nestjs/bullmq'; +import { Module } from '@nestjs/common'; +import { AuthModule } from 'src/modules/auth/auth.module'; +import { AuthSharedController } from 'src/modules/auth/controllers/auth.shared.controller'; +import { AwsModule } from 'src/modules/aws/aws.module'; +import { CountryModule } from 'src/modules/country/country.module'; +import { EmailModule } from 'src/modules/email/email.module'; +import { UserSharedController } from 'src/modules/user/controllers/user.shared.controller'; +import { UserModule } from 'src/modules/user/user.module'; +import { WORKER_CONNECTION_NAME } from 'src/worker/constants/worker.constant'; +import { ENUM_WORKER_QUEUES } from 'src/worker/enums/worker.enum'; + +@Module({ + controllers: [UserSharedController, AuthSharedController], + providers: [], + exports: [], + imports: [ + UserModule, + EmailModule, + AuthModule, + AwsModule, + CountryModule, + BullModule.registerQueue({ + connection: { + name: WORKER_CONNECTION_NAME, + }, + name: ENUM_WORKER_QUEUES.EMAIL_QUEUE, + }), + ], +}) +export class RoutesSharedModule {} diff --git a/src/router/routes/routes.system.module.ts b/src/router/routes/routes.system.module.ts new file mode 100644 index 000000000..dd4447d6f --- /dev/null +++ b/src/router/routes/routes.system.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { CountrySystemController } from 'src/modules/country/controllers/country.system.controller'; +import { CountryModule } from 'src/modules/country/country.module'; +import { HealthSystemController } from 'src/modules/health/controllers/health.system.controller'; +import { HealthModule } from 'src/modules/health/health.module'; +import { RoleSystemController } from 'src/modules/role/controllers/role.system.controller'; +import { RoleModule } from 'src/modules/role/role.module'; +import { SettingSystemController } from 'src/modules/setting/controllers/setting.system.controller'; +import { SettingModule } from 'src/modules/setting/setting.module'; +import { UserModule } from 'src/modules/user/user.module'; + +@Module({ + controllers: [ + HealthSystemController, + SettingSystemController, + CountrySystemController, + RoleSystemController, + ], + providers: [], + exports: [], + imports: [ + HealthModule, + TerminusModule, + SettingModule, + CountryModule, + UserModule, + RoleModule, + ], +}) +export class RoutesSystemModule {} diff --git a/src/router/routes/routes.user.module.ts b/src/router/routes/routes.user.module.ts index 449db3f30..15eb3427b 100644 --- a/src/router/routes/routes.user.module.ts +++ b/src/router/routes/routes.user.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { AuthModule } from 'src/common/auth/auth.module'; +import { AuthModule } from 'src/modules/auth/auth.module'; import { UserUserController } from 'src/modules/user/controllers/user.user.controller'; import { UserModule } from 'src/modules/user/user.module'; diff --git a/src/swagger.ts b/src/swagger.ts index 649f8c904..8b22a479e 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -2,13 +2,13 @@ import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestApplication } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; import { writeFileSync } from 'fs'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; export default async function (app: NestApplication) { const configService = app.get(ConfigService); const env: string = configService.get('app.env'); - const logger = new Logger(); + const logger = new Logger('NestJs-Swagger'); const docName: string = configService.get('doc.name'); const docDesc: string = configService.get('doc.description'); diff --git a/src/worker/constants/worker.constant.ts b/src/worker/constants/worker.constant.ts new file mode 100644 index 000000000..78b8cfa15 --- /dev/null +++ b/src/worker/constants/worker.constant.ts @@ -0,0 +1 @@ +export const WORKER_CONNECTION_NAME = 'PrimaryConnectionWorker'; diff --git a/src/worker/decorators/worker.decorator.ts b/src/worker/decorators/worker.decorator.ts new file mode 100644 index 000000000..301e0d03f --- /dev/null +++ b/src/worker/decorators/worker.decorator.ts @@ -0,0 +1,5 @@ +import { InjectQueue } from '@nestjs/bullmq'; + +export function WorkerQueue(queue: string): ParameterDecorator { + return InjectQueue(queue); +} diff --git a/src/worker/dtos/worker.email.dto.ts b/src/worker/dtos/worker.email.dto.ts new file mode 100644 index 000000000..a7cbb2cf3 --- /dev/null +++ b/src/worker/dtos/worker.email.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsNotEmpty, + IsNotEmptyObject, + IsObject, + IsOptional, + ValidateNested, +} from 'class-validator'; +import { EmailSendDto } from 'src/modules/email/dtos/email.send.dto'; + +export class WorkerEmailDto { + @ApiProperty({ + required: true, + type: () => EmailSendDto, + }) + @IsObject() + @IsNotEmpty() + @IsNotEmptyObject() + @ValidateNested() + @Type(() => EmailSendDto) + send: EmailSendDto; + + @ApiProperty({ + required: false, + type: 'object', + }) + @IsObject() + @IsOptional() + @IsNotEmptyObject() + @ValidateNested() + data?: Record; +} diff --git a/src/worker/enums/worker.enum.ts b/src/worker/enums/worker.enum.ts new file mode 100644 index 000000000..edf1e5630 --- /dev/null +++ b/src/worker/enums/worker.enum.ts @@ -0,0 +1,3 @@ +export enum ENUM_WORKER_QUEUES { + EMAIL_QUEUE = 'EMAIL_QUEUE', +} diff --git a/src/worker/worker.module.ts b/src/worker/worker.module.ts new file mode 100644 index 000000000..83da39235 --- /dev/null +++ b/src/worker/worker.module.ts @@ -0,0 +1,34 @@ +import { BullModule } from '@nestjs/bullmq'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { EmailModule } from 'src/modules/email/email.module'; +import { EmailProcessor } from 'src/modules/email/processors/email.processor'; +import { WORKER_CONNECTION_NAME } from 'src/worker/constants/worker.constant'; + +@Module({ + imports: [ + BullModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + connection: { + name: WORKER_CONNECTION_NAME, + host: configService.get('redis.host'), + port: configService.get('redis.port'), + password: configService.get('redis.password'), + tls: configService.get('redis.tls'), + }, + defaultJobOptions: { + backoff: { + type: 'exponential', + delay: 3000, + }, + attempts: 3, + }, + }), + }), + EmailModule, + ], + providers: [EmailProcessor], +}) +export class WorkerModule {} diff --git a/test/app/dtos/app.dto.spec.ts b/test/app/dtos/app.dto.spec.ts new file mode 100644 index 000000000..895ba5756 --- /dev/null +++ b/test/app/dtos/app.dto.spec.ts @@ -0,0 +1,92 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { AppEnvDto } from 'src/app/dtos/app.env.dto'; +import { + ENUM_APP_ENVIRONMENT, + ENUM_APP_TIMEZONE, +} from 'src/app/enums/app.enum'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; + +describe('AppEnvDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: AppEnvDto = { + APP_NAME: faker.lorem.word(), + APP_ENV: ENUM_APP_ENVIRONMENT.DEVELOPMENT, + APP_LANGUAGE: ENUM_MESSAGE_LANGUAGE.EN, + APP_TIMEZONE: ENUM_APP_TIMEZONE.ASIA_JAKARTA, + APP_DEBUG: false, + + HTTP_ENABLE: true, + HTTP_HOST: 'localhost', + HTTP_PORT: 3000, + + URL_VERSIONING_ENABLE: true, + URL_VERSION: 1, + + DATABASE_URI: faker.internet.url(), + DATABASE_DEBUG: false, + + AUTH_JWT_ISSUER: faker.internet.url(), + AUTH_JWT_AUDIENCE: faker.lorem.word(), + + AUTH_JWT_ACCESS_TOKEN_EXPIRED: '1h', + AUTH_JWT_ACCESS_TOKEN_SECRET_KEY: faker.string.alphanumeric({ + length: 10, + }), + AUTH_JWT_REFRESH_TOKEN_EXPIRED: '182d', + AUTH_JWT_REFRESH_TOKEN_SECRET_KEY: faker.string.alphanumeric({ + length: 10, + }), + + AUTH_SOCIAL_GOOGLE_CLIENT_ID: faker.string.alphanumeric({ + length: 10, + }), + AUTH_SOCIAL_GOOGLE_CLIENT_SECRET: faker.string.alphanumeric({ + length: 10, + }), + + AUTH_SOCIAL_APPLE_CLIENT_ID: faker.string.alphanumeric({ + length: 10, + }), + AUTH_SOCIAL_APPLE_SIGN_IN_CLIENT_ID: faker.string.alphanumeric({ + length: 10, + }), + + AWS_S3_CREDENTIAL_KEY: faker.string.alphanumeric({ + length: 5, + }), + AWS_S3_CREDENTIAL_SECRET: faker.string.alphanumeric({ + length: 10, + }), + AWS_S3_REGION: faker.lorem.word(), + AWS_S3_BUCKET: faker.lorem.word(), + AWS_SES_CREDENTIAL_KEY: faker.string.alphanumeric({ + length: 5, + }), + AWS_SES_CREDENTIAL_SECRET: faker.string.alphanumeric({ + length: 10, + }), + AWS_SES_REGION: faker.lorem.word(), + + REDIS_HOST: faker.internet.url(), + REDIS_PORT: 3001, + REDIS_PASSWORD: faker.string.alphanumeric({ + length: 10, + }), + REDIS_TLS: false, + + SENTRY_DSN: faker.internet.url(), + + CLIENT_URL: faker.internet.url(), + }; + + const dto = plainToInstance(AppEnvDto, response); + + expect(dto).toBeInstanceOf(AppEnvDto); + }); +}); diff --git a/test/app/filters/app.general.filter.spec.ts b/test/app/filters/app.general.filter.spec.ts new file mode 100644 index 000000000..63278b1d0 --- /dev/null +++ b/test/app/filters/app.general.filter.spec.ts @@ -0,0 +1,221 @@ +import { ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpAdapterHost } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { SentryService } from '@ntegral/nestjs-sentry'; +import { Response } from 'express'; +import { MessageService } from 'src/common/message/services/message.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { RequestValidationException } from 'src/common/request/exceptions/request.validation.exception'; +import { FileImportException } from 'src/common/file/exceptions/file.import.exception'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppGeneralFilter } from 'src/app/filters/app.general.filter'; +import { ValidationError } from 'class-validator'; +import { IMessageValidationImportErrorParam } from 'src/common/message/interfaces/message.interface'; + +describe('AppGeneralFilter', () => { + describe('Debug is on', () => { + let appGeneralFilter: AppGeneralFilter; + let mockHttpAdapterHost: HttpAdapterHost; + let mockMessageService: MessageService; + let mockConfigService: ConfigService; + let mockHelperDateService: HelperDateService; + let mockSentryService: SentryService; + let mockResponse: Response; + let mockRequest: IRequestApp; + + beforeEach(async () => { + mockHttpAdapterHost = { httpAdapter: { reply: jest.fn() } } as any; + mockMessageService = { + getLanguage: jest.fn(), + setMessage: jest.fn(), + } as any; + mockConfigService = { + get: jest.fn().mockReturnValue(false), + } as any; + mockHelperDateService = { createTimestamp: jest.fn() } as any; + mockSentryService = { + instance: () => ({ captureException: jest.fn() }), + } as any; + mockResponse = { + setHeader: jest.fn(), + status: jest.fn(), + json: jest.fn(), + } as any; + mockRequest = { + __language: null, + path: '/test', + __version: null, + } as IRequestApp; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppGeneralFilter, + { provide: HttpAdapterHost, useValue: mockHttpAdapterHost }, + { provide: MessageService, useValue: mockMessageService }, + { provide: ConfigService, useValue: mockConfigService }, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { provide: SentryService, useValue: mockSentryService }, + ], + }).compile(); + + appGeneralFilter = module.get(AppGeneralFilter); + + jest.spyOn( + appGeneralFilter['logger'], + 'error' + ).mockImplementation(); + }); + + it('should return response', async () => { + const mockException = new Error('Test error'); + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + (mockResponse.setHeader as jest.Mock).mockReturnValue(mockResponse); + (mockResponse.status as jest.Mock).mockReturnValue(mockResponse); + jest.spyOn(mockResponse, 'json').mockReturnValue(mockResponse); + + await appGeneralFilter.catch(mockException, mockArgumentsHost); + + expect(appGeneralFilter['logger'].error).not.toHaveBeenCalled(); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should not send validation or import exceptions to Sentry', async () => { + const validationException = new RequestValidationException( + [] as ValidationError[] + ); + const importException = new FileImportException( + [] as IMessageValidationImportErrorParam[] + ); + + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + (mockResponse.setHeader as jest.Mock).mockReturnValue(mockResponse); + (mockResponse.status as jest.Mock).mockReturnValue(mockResponse); + jest.spyOn(mockResponse, 'json').mockReturnValue(mockResponse); + + await appGeneralFilter.catch( + validationException, + mockArgumentsHost + ); + await appGeneralFilter.catch(importException, mockArgumentsHost); + + expect(appGeneralFilter['logger'].error).not.toHaveBeenCalled(); + expect( + mockSentryService.instance().captureException + ).not.toHaveBeenCalled(); + }); + + it('should respond with the exception response if it is an HttpException', async () => { + const httpException = new HttpException( + 'Not Found', + HttpStatus.NOT_FOUND + ); + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + await appGeneralFilter.catch(httpException, mockArgumentsHost); + + expect(appGeneralFilter['logger'].error).not.toHaveBeenCalled(); + expect(mockHttpAdapterHost.httpAdapter.reply).toHaveBeenCalledWith( + mockResponse, + 'Not Found', + HttpStatus.NOT_FOUND + ); + }); + }); + + describe('Debug is off', () => { + let appGeneralFilter: AppGeneralFilter; + let mockHttpAdapterHost: HttpAdapterHost; + let mockMessageService: MessageService; + let mockConfigService: ConfigService; + let mockHelperDateService: HelperDateService; + let mockSentryService: SentryService; + let mockResponse: Response; + let mockRequest: IRequestApp; + + beforeEach(async () => { + mockHttpAdapterHost = { httpAdapter: { reply: jest.fn() } } as any; + mockMessageService = { + getLanguage: jest.fn(), + setMessage: jest.fn(), + } as any; + mockConfigService = { + get: jest.fn().mockReturnValue(true), + } as any; + mockHelperDateService = { createTimestamp: jest.fn() } as any; + mockSentryService = { + instance: () => ({ captureException: jest.fn() }), + } as any; + mockResponse = { + setHeader: jest.fn(), + status: jest.fn(), + json: jest.fn(), + } as any; + mockRequest = { + __language: null, + path: '/test', + __version: null, + } as IRequestApp; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppGeneralFilter, + { provide: HttpAdapterHost, useValue: mockHttpAdapterHost }, + { provide: MessageService, useValue: mockMessageService }, + { provide: ConfigService, useValue: mockConfigService }, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { provide: SentryService, useValue: mockSentryService }, + ], + }).compile(); + + appGeneralFilter = module.get(AppGeneralFilter); + + jest.spyOn( + appGeneralFilter['logger'], + 'error' + ).mockImplementation(); + }); + + it('should log error if in debug mode', async () => { + const mockException = new Error('Test error'); + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + (mockResponse.setHeader as jest.Mock).mockReturnValue(mockResponse); + (mockResponse.status as jest.Mock).mockReturnValue(mockResponse); + jest.spyOn(mockResponse, 'json').mockReturnValue(mockResponse); + + await appGeneralFilter.catch(mockException, mockArgumentsHost); + + expect(appGeneralFilter['logger'].error).toHaveBeenCalled(); + expect(mockResponse.json).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/app/filters/app.http.filter.spec.ts b/test/app/filters/app.http.filter.spec.ts new file mode 100644 index 000000000..9269ed5e9 --- /dev/null +++ b/test/app/filters/app.http.filter.spec.ts @@ -0,0 +1,144 @@ +import { ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { Response } from 'express'; +import { MessageService } from 'src/common/message/services/message.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppHttpFilter } from 'src/app/filters/app.http.filter'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; + +describe('AppHttpFilter', () => { + let appHttpFilter: AppHttpFilter; + let mockMessageService: MessageService; + let mockConfigService: ConfigService; + let mockHelperDateService: HelperDateService; + let mockResponse: Response; + let mockRequest: IRequestApp; + + beforeEach(async () => { + mockMessageService = { + getLanguage: jest.fn(), + setMessage: jest.fn(), + } as any; + mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + const config = { + 'app.debug': true, + 'app.globalPrefix': 'api', + 'doc.prefix': 'docs', + }; + return config[key]; + }), + } as any; + mockHelperDateService = { createTimestamp: jest.fn() } as any; + mockResponse = { + setHeader: jest.fn(), + status: jest.fn(), + json: jest.fn(), + redirect: jest.fn(), + } as any; + mockRequest = { + __language: 'en', + path: '/test', + __version: '1.0', + } as IRequestApp; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppHttpFilter, + { provide: MessageService, useValue: mockMessageService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: HelperDateService, useValue: mockHelperDateService }, + ], + }).compile(); + + appHttpFilter = module.get(AppHttpFilter); + + jest.spyOn(appHttpFilter['logger'], 'error').mockImplementation(); + }); + + it('should log error if in debug mode', async () => { + const mockException = new HttpException( + 'Test error', + HttpStatus.BAD_REQUEST + ); + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + await appHttpFilter.catch(mockException, mockArgumentsHost); + expect(mockResponse.redirect).toHaveBeenCalledWith( + HttpStatus.PERMANENT_REDIRECT, + 'docs' + ); + }); + + it('should handle custom exceptions properly', async () => { + jest.spyOn(mockHelperDateService, 'createTimestamp').mockReturnValue( + Date.now() + ); + jest.spyOn(mockMessageService, 'setMessage').mockReturnValue( + 'Custom Error' + ); + jest.spyOn(mockMessageService, 'getLanguage').mockReturnValue( + ENUM_MESSAGE_LANGUAGE.EN + ); + + const customExceptionResponse = { + statusCode: HttpStatus.BAD_REQUEST, + message: 'Custom Error', + _metadata: { customProperty: { messageProperties: {} } }, + data: { custom: 'data' }, + }; + const mockException = new HttpException( + customExceptionResponse, + HttpStatus.BAD_REQUEST + ); + + const mockResponse = { + setHeader: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + redirect: jest.fn(), + } as unknown as Response; + + const mockRequest = { + path: 'docs', + __language: null, + __version: null, + } as IRequestApp; + appHttpFilter['app.debug'] = true; + appHttpFilter['app.globalPrefix'] = '/path'; + + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + await appHttpFilter.catch(mockException, mockArgumentsHost); + + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-custom-lang', + 'en' + ); + }); + + it('isErrorException return true', () => { + expect( + appHttpFilter.isErrorException({ + statusCode: 200, + message: 'message', + }) + ).toBe(true); + }); + + it('isErrorException return false', () => { + expect(appHttpFilter.isErrorException('')).toBe(false); + }); +}); diff --git a/test/app/filters/app.validation-import.filter.spec.ts b/test/app/filters/app.validation-import.filter.spec.ts new file mode 100644 index 000000000..c64f3679e --- /dev/null +++ b/test/app/filters/app.validation-import.filter.spec.ts @@ -0,0 +1,122 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FileImportException } from 'src/common/file/exceptions/file.import.exception'; +import { MessageService } from 'src/common/message/services/message.service'; +import { ConfigService } from '@nestjs/config'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppValidationImportFilter } from 'src/app/filters/app.validation-import.filter'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; + +class MockMessageService { + getLanguage = jest.fn().mockReturnValue('en'); + setMessage = jest.fn().mockImplementation((message: string) => message); + setValidationImportMessage = jest.fn().mockImplementation(errors => errors); +} + +class MockConfigService { + get = jest.fn().mockImplementation((key: string) => { + const config = { + 'app.debug': true, + 'app.urlVersion.version': '1.0', + 'app.repoVersion': 'v1.0.0', + }; + return config[key]; + }); +} + +class MockHelperDateService { + createTimestamp = jest.fn().mockReturnValue(new Date().toISOString()); +} + +describe('AppValidationImportFilter', () => { + let filter: AppValidationImportFilter; + let mockMessageService: MockMessageService; + let mockConfigService: MockConfigService; + let mockHelperDateService: MockHelperDateService; + + beforeEach(async () => { + mockMessageService = new MockMessageService(); + mockConfigService = new MockConfigService(); + mockHelperDateService = new MockHelperDateService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppValidationImportFilter, + { provide: MessageService, useValue: mockMessageService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: HelperDateService, useValue: mockHelperDateService }, + ], + }).compile(); + + filter = module.get( + AppValidationImportFilter + ); + + jest.spyOn(filter['logger'], 'error').mockImplementation(); + }); + + it('should be defined', () => { + expect(filter).toBeDefined(); + }); + + it('should handle FileImportException and set response correctly', async () => { + const exception = new FileImportException([]); + + const mockResponse = { + setHeader: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + } as unknown as Response; + + const mockRequest = { + path: '/upload', + __language: null, + __version: null, + } as IRequestApp; + + const now = new Date(); + jest.spyOn(mockHelperDateService, 'createTimestamp').mockReturnValue( + now + ); + + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as unknown as ArgumentsHost; + + await filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-custom-lang', + 'en' + ); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-timestamp', now); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-timezone', + 'UTC' + ); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-version', '1.0'); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-repo-version', + 'v1.0.0' + ); + expect(mockResponse.status).toHaveBeenCalledWith(422); + expect(mockResponse.json).toHaveBeenCalledWith({ + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION, + message: 'file.error.validationDto', + errors: [], + _metadata: { + language: 'en', + timestamp: now, + timezone: 'UTC', + path: '/upload', + version: '1.0', + repoVersion: 'v1.0.0', + }, + }); + }); +}); diff --git a/test/app/filters/app.validation.filter.spec.ts b/test/app/filters/app.validation.filter.spec.ts new file mode 100644 index 000000000..a903b37e1 --- /dev/null +++ b/test/app/filters/app.validation.filter.spec.ts @@ -0,0 +1,121 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MessageService } from 'src/common/message/services/message.service'; +import { ConfigService } from '@nestjs/config'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { RequestValidationException } from 'src/common/request/exceptions/request.validation.exception'; +import { ArgumentsHost, HttpStatus } from '@nestjs/common'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppValidationFilter } from 'src/app/filters/app.validation.filter'; + +class MockMessageService { + getLanguage = jest.fn().mockReturnValue('en'); + setMessage = jest.fn().mockImplementation((message: string) => message); + setValidationMessage = jest.fn().mockImplementation(errors => errors); +} + +class MockConfigService { + get = jest.fn().mockImplementation((key: string) => { + const config = { + 'app.debug': true, + 'app.urlVersion.version': '1.0', + 'app.repoVersion': 'v1.0.0', + }; + return config[key]; + }); +} + +class MockHelperDateService { + createTimestamp = jest.fn().mockReturnValue(new Date().toISOString()); +} + +describe('AppValidationFilter', () => { + let filter: AppValidationFilter; + let mockMessageService: MockMessageService; + let mockConfigService: MockConfigService; + let mockHelperDateService: MockHelperDateService; + + beforeEach(async () => { + mockMessageService = new MockMessageService(); + mockConfigService = new MockConfigService(); + mockHelperDateService = new MockHelperDateService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppValidationFilter, + { provide: MessageService, useValue: mockMessageService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: HelperDateService, useValue: mockHelperDateService }, + ], + }).compile(); + + filter = module.get(AppValidationFilter); + + jest.spyOn(filter['logger'], 'error').mockImplementation(); + }); + + it('should be defined', () => { + expect(filter).toBeDefined(); + }); + + it('should handle RequestValidationException and set response correctly', async () => { + const exception = new RequestValidationException([]); + + const mockResponse = { + setHeader: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + } as unknown as Response; + + const mockRequest = { + path: '/test', + __language: null, + __version: null, + } as IRequestApp; + + const now = new Date(); + jest.spyOn(mockHelperDateService, 'createTimestamp').mockReturnValue( + now + ); + + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as unknown as ArgumentsHost; + + await filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-custom-lang', + 'en' + ); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-timestamp', now); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-timezone', + 'UTC' + ); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-version', '1.0'); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'x-repo-version', + 'v1.0.0' + ); + expect(mockResponse.status).toHaveBeenCalledWith( + HttpStatus.UNPROCESSABLE_ENTITY + ); + expect(mockResponse.json).toHaveBeenCalledWith({ + statusCode: 5030, + message: 'request.validation', + errors: [], + _metadata: { + language: 'en', + timestamp: now, + timezone: 'UTC', + path: '/test', + version: '1.0', + repoVersion: 'v1.0.0', + }, + }); + }); +}); diff --git a/test/app/middlewares/app.body-parser.middleware.spec.ts b/test/app/middlewares/app.body-parser.middleware.spec.ts new file mode 100644 index 000000000..2a86771d7 --- /dev/null +++ b/test/app/middlewares/app.body-parser.middleware.spec.ts @@ -0,0 +1,201 @@ +import { ExecutionContext } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { ConfigService } from '@nestjs/config'; +import bodyParser from 'body-parser'; +import { + AppJsonBodyParserMiddleware, + AppRawBodyParserMiddleware, + AppTextBodyParserMiddleware, + AppUrlencodedBodyParserMiddleware, +} from 'src/app/middlewares/app.body-parser.middleware'; + +/* eslint-disable */ +jest.mock('body-parser', () => ({ + urlencoded: jest + .fn() + .mockImplementation((a, b, c: () => null) => jest.fn()), + raw: jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()), + text: jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()), + json: jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()), +})); +/* eslint-enable */ + +describe('AppUrlencodedBodyParserMiddleware', () => { + let middleware: AppUrlencodedBodyParserMiddleware; + + const mockConfigService = { + get: jest.fn().mockReturnValue(1000), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppUrlencodedBodyParserMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppUrlencodedBodyParserMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute bodyParser urlencoded', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + jest.spyOn(bodyParser, 'urlencoded').mockReturnValue(jest.fn()); + middleware.use(request, response, next); + + expect(bodyParser.urlencoded).toHaveBeenCalledWith({ + extended: false, + limit: 1000, + }); + }); + }); +}); + +describe('AppJsonBodyParserMiddleware', () => { + let middleware: AppJsonBodyParserMiddleware; + + const mockConfigService = { + get: jest.fn().mockReturnValue(1000), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppJsonBodyParserMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppJsonBodyParserMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute bodyParser json', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + jest.spyOn(bodyParser, 'json').mockReturnValue(jest.fn()); + middleware.use(request, response, next); + + expect(bodyParser.json).toHaveBeenCalledWith({ + limit: 1000, + }); + }); + }); +}); + +describe('AppRawBodyParserMiddleware', () => { + let middleware: AppRawBodyParserMiddleware; + + const mockConfigService = { + get: jest.fn().mockReturnValue(1000), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppRawBodyParserMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppRawBodyParserMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute bodyParser raw', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + jest.spyOn(bodyParser, 'raw').mockReturnValue(jest.fn()); + middleware.use(request, response, next); + + expect(bodyParser.raw).toHaveBeenCalledWith({ + limit: 1000, + }); + }); + }); +}); + +describe('AppTextBodyParserMiddleware', () => { + let middleware: AppTextBodyParserMiddleware; + + const mockConfigService = { + get: jest.fn().mockReturnValue(1000), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppTextBodyParserMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppTextBodyParserMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute bodyParser text', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + jest.spyOn(bodyParser, 'text').mockReturnValue(jest.fn()); + middleware.use(request, response, next); + + expect(bodyParser.text).toHaveBeenCalledWith({ + limit: 1000, + }); + }); + }); +}); diff --git a/test/app/middlewares/app.cors.middleware.spec.ts b/test/app/middlewares/app.cors.middleware.spec.ts new file mode 100644 index 000000000..fcc4b91e0 --- /dev/null +++ b/test/app/middlewares/app.cors.middleware.spec.ts @@ -0,0 +1,148 @@ +import { ExecutionContext, HttpStatus } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { ConfigService } from '@nestjs/config'; +import { Response } from 'express'; +import { AppCorsMiddleware } from 'src/app/middlewares/app.cors.middleware'; +import cors from 'cors'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; + +/* eslint-disable */ +jest.mock('cors', () => + jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()) +); +/* eslint-enable */ + +describe('AppCorsMiddleware On Development', () => { + const allowMethod = ['GET']; + const allowOrigin = 'example.com'; + const allowHeader = ['Accept']; + + let middleware: AppCorsMiddleware; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.DEVELOPMENT; + case 'middleware.cors.allowOrigin': + return allowOrigin; + case 'middleware.cors.allowMethod': + return allowMethod; + default: + return allowHeader; + } + }), + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + AppCorsMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get(AppCorsMiddleware); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should allow * on development env', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(cors).toHaveBeenCalledWith({ + origin: '*', + methods: allowMethod, + allowedHeaders: allowHeader, + preflightContinue: false, + credentials: true, + optionsSuccessStatus: HttpStatus.NO_CONTENT, + }); + }); + }); +}); + +describe('AppCorsMiddleware On Production', () => { + const allowMethod = ['GET']; + const allowOrigin = 'example.com'; + const allowHeader = ['Accept']; + + let middleware: AppCorsMiddleware; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.PRODUCTION; + case 'middleware.cors.allowOrigin': + return allowOrigin; + case 'middleware.cors.allowMethod': + return allowMethod; + default: + return allowHeader; + } + }), + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + AppCorsMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get(AppCorsMiddleware); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should allow origin from configs on production env', () => { + const context = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + headers: {}, + }), + getResponse: jest.fn().mockReturnValue({ + setHeader: jest.fn(), + getHeader: jest.fn(), + }), + }), + }); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(cors).toHaveBeenCalledWith({ + origin: allowOrigin, + methods: allowMethod, + allowedHeaders: allowHeader, + preflightContinue: false, + credentials: true, + optionsSuccessStatus: HttpStatus.NO_CONTENT, + }); + }); + }); +}); diff --git a/test/app/middlewares/app.custom-language.middleware.spec.ts b/test/app/middlewares/app.custom-language.middleware.spec.ts new file mode 100644 index 000000000..332f850c1 --- /dev/null +++ b/test/app/middlewares/app.custom-language.middleware.spec.ts @@ -0,0 +1,88 @@ +import { ExecutionContext } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppCustomLanguageMiddleware } from 'src/app/middlewares/app.custom-language.middleware'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { ConfigService } from '@nestjs/config'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; + +describe('AppCustomLanguageMiddleware', () => { + let middleware: AppCustomLanguageMiddleware; + + const mockConfigService = { + get: jest.fn().mockImplementation(e => { + switch (e) { + case 'message.language': + return ENUM_MESSAGE_LANGUAGE.EN; + default: + return [ENUM_MESSAGE_LANGUAGE.EN]; + } + }), + }; + + const mockHelperArrayService = { + getIntersection: jest.fn().mockReturnValue([ENUM_MESSAGE_LANGUAGE.EN]), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppCustomLanguageMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: HelperArrayService, + useValue: mockHelperArrayService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppCustomLanguageMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should put x-custom-lang into Request instance', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + request.headers['x-custom-lang'] = ENUM_MESSAGE_LANGUAGE.EN; + middleware.use(request, response, next); + + expect(request.__language).toBeDefined(); + expect(request.__language).toBe(ENUM_MESSAGE_LANGUAGE.EN); + expect(request.headers['x-custom-lang']).toBeDefined(); + expect(request.headers['x-custom-lang']).toBe( + ENUM_MESSAGE_LANGUAGE.EN + ); + }); + + it('should set custom language from Request', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + request.headers['x-custom-lang'] = ENUM_MESSAGE_LANGUAGE.EN; + middleware.use(request, response, next); + + expect(request.__language).toBeDefined(); + expect(request.__language).toBe(ENUM_MESSAGE_LANGUAGE.EN); + expect(request.headers['x-custom-lang']).toBeDefined(); + expect(request.headers['x-custom-lang']).toBe( + ENUM_MESSAGE_LANGUAGE.EN + ); + }); + }); +}); diff --git a/test/app/middlewares/app.helmet.middleware.spec.ts b/test/app/middlewares/app.helmet.middleware.spec.ts new file mode 100644 index 000000000..5e76836a0 --- /dev/null +++ b/test/app/middlewares/app.helmet.middleware.spec.ts @@ -0,0 +1,42 @@ +import { ExecutionContext } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { AppHelmetMiddleware } from 'src/app/middlewares/app.helmet.middleware'; +import helmet from 'helmet'; + +/* eslint-disable */ +jest.mock('helmet', () => + jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()) +); +/* eslint-enable */ + +describe('AppHelmetMiddleware', () => { + let middleware: AppHelmetMiddleware; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [AppHelmetMiddleware], + }).compile(); + + middleware = moduleRef.get(AppHelmetMiddleware); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute next', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(helmet).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/app/middlewares/app.response-time.middleware.spec.ts b/test/app/middlewares/app.response-time.middleware.spec.ts new file mode 100644 index 000000000..0af3ab3aa --- /dev/null +++ b/test/app/middlewares/app.response-time.middleware.spec.ts @@ -0,0 +1,44 @@ +import { ExecutionContext } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AppResponseTimeMiddleware } from 'src/app/middlewares/app.response-time.middleware'; +import responseTime from 'response-time'; + +/* eslint-disable */ +jest.mock('response-time', () => + jest.fn().mockImplementation((a, b, c: () => null) => jest.fn()) +); +/* eslint-enable */ + +describe('AppResponseTimeMiddleware', () => { + let middleware: AppResponseTimeMiddleware; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [AppResponseTimeMiddleware], + }).compile(); + + middleware = moduleRef.get( + AppResponseTimeMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should execute next', () => { + const context = createMock(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(responseTime).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/app/middlewares/app.url-version.middleware.spec.ts b/test/app/middlewares/app.url-version.middleware.spec.ts new file mode 100644 index 000000000..9ddc4d3c9 --- /dev/null +++ b/test/app/middlewares/app.url-version.middleware.spec.ts @@ -0,0 +1,183 @@ +import { ExecutionContext } from '@nestjs/common'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { ConfigService } from '@nestjs/config'; +import { AppUrlVersionMiddleware } from 'src/app/middlewares/app.url-version.middleware'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; + +describe('AppUrlVersionMiddleware', () => { + describe('Development Environment', () => { + let middleware: AppUrlVersionMiddleware; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.DEVELOPMENT; + case 'app.globalPrefix': + return '/api'; + case 'app.urlVersion.enable': + return true; + case 'app.urlVersion.prefix': + return 'v'; + case 'app.urlVersion.version': + return '1'; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppUrlVersionMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppUrlVersionMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should put default api version if not in /api prefix', () => { + const context = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + originalUrl: '/', + }), + getResponse: jest.fn(), + }), + }); + const request = context + .switchToHttp() + .getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(middleware['env']).toBe( + ENUM_APP_ENVIRONMENT.DEVELOPMENT + ); + expect(middleware['globalPrefix']).toBe('/api'); + expect(middleware['urlVersionEnable']).toBe(true); + expect(middleware['urlVersionPrefix']).toBe('v'); + expect(middleware['urlVersion']).toBe('1'); + expect(request.__version).toBe('1'); + expect(next).toHaveBeenCalled(); + }); + + it('should put default api version base on /api/v{version} prefix', () => { + const context = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + originalUrl: '/api/v2', + }), + getResponse: jest.fn(), + }), + }); + const request = context + .switchToHttp() + .getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(middleware['env']).toBe( + ENUM_APP_ENVIRONMENT.DEVELOPMENT + ); + + expect(middleware['globalPrefix']).toBe('/api'); + expect(middleware['urlVersionEnable']).toBe(true); + expect(middleware['urlVersionPrefix']).toBe('v'); + expect(middleware['urlVersion']).toBe('1'); + expect(request.__version).toBe('2'); + expect(next).toHaveBeenCalled(); + }); + }); + }); + + describe('Production Environment', () => { + let middleware: AppUrlVersionMiddleware; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.PRODUCTION; + case 'app.globalPrefix': + return ''; + case 'app.urlVersion.enable': + return true; + case 'app.urlVersion.prefix': + return 'v'; + case 'app.urlVersion.version': + return '1'; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + AppUrlVersionMiddleware, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + middleware = moduleRef.get( + AppUrlVersionMiddleware + ); + }); + + it('should be defined', () => { + expect(middleware).toBeDefined(); + }); + + describe('use', () => { + it('should put default api version base on /api/v{version} prefix', () => { + const context = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + originalUrl: '/v2', + }), + getResponse: jest.fn(), + }), + }); + const request = context + .switchToHttp() + .getRequest(); + const response = context.switchToHttp().getResponse(); + const next = jest.fn(); + + middleware.use(request, response, next); + + expect(middleware['env']).toBe(ENUM_APP_ENVIRONMENT.PRODUCTION); + expect(middleware['globalPrefix']).toBe(''); + expect(middleware['urlVersionEnable']).toBe(true); + expect(middleware['urlVersionPrefix']).toBe('v'); + expect(middleware['urlVersion']).toBe('1'); + expect(request.__version).toBe('2'); + expect(next).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/test/common/database/decorators/database.decorator.spec.ts b/test/common/database/decorators/database.decorator.spec.ts new file mode 100644 index 000000000..c598a9df3 --- /dev/null +++ b/test/common/database/decorators/database.decorator.spec.ts @@ -0,0 +1,220 @@ +import { + InjectConnection, + InjectModel, + Prop, + Schema, + SchemaFactory, +} from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + DatabaseConnection, + DatabaseEntity, + DatabaseModel, + DatabaseProp, + DatabaseQueryAnd, + DatabaseQueryContain, + DatabaseQueryEqual, + DatabaseQueryIn, + DatabaseQueryNin, + DatabaseQueryNotEqual, + DatabaseQueryOr, + DatabaseSchema, +} from 'src/common/database/decorators/database.decorator'; + +jest.mock('@nestjs/mongoose', () => ({ + ...jest.requireActual('@nestjs/mongoose'), + InjectConnection: jest.fn(), + InjectModel: jest.fn(), + Schema: jest.fn(), + SchemaFactory: { + createForClass: jest.fn(), + }, + Prop: jest.fn(), +})); + +describe('Database Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('DatabaseConnection', () => { + it('Should return applyDecorators', async () => { + DatabaseConnection(); + + expect(InjectConnection).toHaveBeenCalledWith( + DATABASE_CONNECTION_NAME + ); + }); + + it('Should return applyDecorators with options', async () => { + DatabaseConnection('test-connection'); + + expect(InjectConnection).toHaveBeenCalledWith('test-connection'); + }); + }); + + describe('DatabaseModel', () => { + it('Should return applyDecorators', async () => { + DatabaseModel('entity-name'); + + expect(InjectModel).toHaveBeenCalledWith( + 'entity-name', + DATABASE_CONNECTION_NAME + ); + }); + + it('Should return applyDecorators with options', async () => { + DatabaseModel('entity-name', 'test-connection'); + + expect(InjectModel).toHaveBeenCalledWith( + 'entity-name', + 'test-connection' + ); + }); + }); + + describe('DatabaseEntity', () => { + it('Should return applyDecorators', async () => { + DatabaseEntity(); + + expect(Schema).toHaveBeenCalledWith({ + timestamps: { + createdAt: true, + updatedAt: true, + }, + }); + }); + + it('Should return applyDecorators with options', async () => { + DatabaseEntity({ + _id: false, + }); + + expect(Schema).toHaveBeenCalledWith({ + _id: false, + timestamps: { + createdAt: true, + updatedAt: true, + }, + }); + }); + }); + + describe('DatabaseProp', () => { + it('Should return applyDecorators', async () => { + DatabaseProp(); + + expect(Prop).toHaveBeenCalled(); + }); + + it('Should return applyDecorators with options', async () => { + DatabaseProp({ + _id: false, + }); + + expect(Prop).toHaveBeenCalledWith({ + _id: false, + }); + }); + }); + + describe('DatabaseSchema', () => { + it('Should return a mongoose schema', async () => { + DatabaseSchema({} as any); + + expect(SchemaFactory.createForClass).toHaveBeenCalledWith({}); + }); + }); + + describe('DatabaseQueryIn', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryIn('test', ['name', 'status']); + + expect(result).toEqual({ + test: { + $in: ['name', 'status'], + }, + }); + }); + }); + + describe('DatabaseQueryNin', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryNin('test', ['name', 'status']); + + expect(result).toEqual({ + test: { + $nin: ['name', 'status'], + }, + }); + }); + }); + + describe('DatabaseQueryEqual', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryEqual('test', 'name'); + + expect(result).toEqual({ + test: 'name', + }); + }); + }); + + describe('DatabaseQueryNotEqual', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryNotEqual('test', 'name'); + + expect(result).toEqual({ + test: { + $ne: 'name', + }, + }); + }); + }); + + describe('DatabaseQueryContain', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryContain('test', 'name'); + + expect(result).toEqual({ + test: { + $regex: new RegExp('name'), + $options: 'i', + }, + }); + }); + + it('Should convert query request to mongodb query with full match', async () => { + const result = DatabaseQueryContain('test', 'name', { + fullWord: true, + }); + + expect(result).toEqual({ + test: { + $regex: new RegExp('\\bname\\b'), + $options: 'i', + }, + }); + }); + }); + + describe('DatabaseQueryOr', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryOr([]); + + expect(result).toEqual({ + $or: [], + }); + }); + }); + + describe('DatabaseQueryAnd', () => { + it('Should convert query request to mongodb query', async () => { + const result = DatabaseQueryAnd([]); + + expect(result).toEqual({ + $and: [], + }); + }); + }); +}); diff --git a/test/common/database/dtos/database.dto.spec.ts b/test/common/database/dtos/database.dto.spec.ts new file mode 100644 index 000000000..c2da119e6 --- /dev/null +++ b/test/common/database/dtos/database.dto.spec.ts @@ -0,0 +1,25 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { DatabaseDto } from 'src/common/database/dtos/database.dto'; + +describe('DatabaseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: DatabaseDto = { + _id: faker.string.uuid(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + deletedBy: faker.string.uuid(), + deleted: false, + }; + + const dto = plainToInstance(DatabaseDto, response); + + expect(dto).toBeInstanceOf(DatabaseDto); + expect(dto._id).toBeDefined(); + }); +}); diff --git a/test/common/database/dtos/database.soft-delete.dto.spec.ts b/test/common/database/dtos/database.soft-delete.dto.spec.ts new file mode 100644 index 000000000..2bb604336 --- /dev/null +++ b/test/common/database/dtos/database.soft-delete.dto.spec.ts @@ -0,0 +1,20 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { DatabaseSoftDeleteDto } from 'src/common/database/dtos/database.soft-delete.dto'; + +describe('DatabaseSoftDeleteDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: DatabaseSoftDeleteDto = { + deletedBy: faker.string.uuid(), + }; + + const dto = plainToInstance(DatabaseSoftDeleteDto, response); + + expect(dto).toBeInstanceOf(DatabaseSoftDeleteDto); + expect(dto.deletedBy).toBeDefined(); + }); +}); diff --git a/test/common/database/dtos/response/database.id.response.spec.ts b/test/common/database/dtos/response/database.id.response.spec.ts new file mode 100644 index 000000000..c1683bb41 --- /dev/null +++ b/test/common/database/dtos/response/database.id.response.spec.ts @@ -0,0 +1,20 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { DatabaseIdResponseDto } from 'src/common/database/dtos/response/database.id.response.dto'; + +describe('DatabaseIdResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: DatabaseIdResponseDto = { + _id: faker.string.uuid(), + }; + + const dto = plainToInstance(DatabaseIdResponseDto, response); + + expect(dto).toBeInstanceOf(DatabaseIdResponseDto); + expect(dto._id).toBeDefined(); + }); +}); diff --git a/test/common/database/services/database.service.spec.ts b/test/common/database/services/database.service.spec.ts new file mode 100644 index 000000000..bc13cad25 --- /dev/null +++ b/test/common/database/services/database.service.spec.ts @@ -0,0 +1,127 @@ +import { faker } from '@faker-js/faker'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import mongoose from 'mongoose'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/enums/app.enum'; +import { DatabaseService } from 'src/common/database/services/database.service'; + +jest.mock('mongoose', () => ({ + ...jest.requireActual('mongoose'), + set: jest.fn(), +})); + +describe('DatabaseService', () => { + describe('Production Environment', () => { + let service: DatabaseService; + const databaseUrl = faker.internet.url(); + + const mockConfigServiceProduction = { + get: jest.fn().mockImplementation(e => { + switch (e) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.PRODUCTION; + case 'database.uri': + return databaseUrl; + case 'database.debug': + return false; + case 'database.timeoutOptions': + return {}; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRefRef = await Test.createTestingModule({ + providers: [ + DatabaseService, + { + provide: ConfigService, + useValue: mockConfigServiceProduction, + }, + ], + }).compile(); + + service = moduleRefRef.get(DatabaseService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createOptions', () => { + it('should return mongoose options', () => { + const result = service.createOptions(); + + expect(mongoose.set).toHaveBeenCalledTimes(0); + expect(result).toEqual({ + uri: databaseUrl, + autoCreate: false, + autoIndex: false, + }); + }); + }); + }); + + describe('Development Environment', () => { + let service: DatabaseService; + const databaseUrl = faker.internet.url(); + + const mockConfigServiceDevelopment = { + get: jest.fn().mockImplementation(e => { + switch (e) { + case 'app.env': + return ENUM_APP_ENVIRONMENT.DEVELOPMENT; + case 'database.uri': + return databaseUrl; + case 'database.debug': + return true; + case 'database.timeoutOptions': + return {}; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRefRef = await Test.createTestingModule({ + providers: [ + DatabaseService, + { + provide: ConfigService, + useValue: mockConfigServiceDevelopment, + }, + ], + }).compile(); + + service = moduleRefRef.get(DatabaseService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createOptions', () => { + it('should return mongoose options and debug on', () => { + const result = service.createOptions(); + + expect(mongoose.set).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + uri: databaseUrl, + autoCreate: false, + autoIndex: false, + }); + }); + }); + }); +}); diff --git a/test/common/doc/decorators/doc.decorator.spec.ts b/test/common/doc/decorators/doc.decorator.spec.ts new file mode 100644 index 000000000..e10ecc5f8 --- /dev/null +++ b/test/common/doc/decorators/doc.decorator.spec.ts @@ -0,0 +1,1034 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiHeaders, + ApiOperation, + ApiParam, + ApiProduces, + ApiQuery, + ApiResponse, + getSchemaPath, +} from '@nestjs/swagger'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ENUM_APP_STATUS_CODE_ERROR } from 'src/app/enums/app.status-code.enum'; +import { + Doc, + DocAllOf, + DocAnyOf, + DocAuth, + DocDefault, + DocErrorGroup, + DocGuard, + DocOneOf, + DocRequest, + DocRequestFile, + DocResponse, + DocResponseFile, + DocResponsePaging, +} from 'src/common/doc/decorators/doc.decorator'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/enums/doc.enum'; +import { + IDocGuardOptions, + IDocOfOptions, + IDocRequestFileOptions, + IDocRequestOptions, +} from 'src/common/doc/interfaces/doc.interface'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; +import { ResponseDto } from 'src/common/response/dtos/response.dto'; +import { ResponsePagingDto } from 'src/common/response/dtos/response.paging.dto'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; + +describe('DocDecorators', () => { + let moduleRef: TestingModule; + + beforeAll(async () => { + moduleRef = await Test.createTestingModule({}).compile(); + }); + + afterAll(async () => { + await moduleRef.close(); + }); + + describe('DocDefault', () => { + it('should create a decorator with the correct ApiResponse schema', () => { + const options = { + messagePath: 'test.message', + statusCode: HttpStatus.OK, + httpStatus: HttpStatus.OK, + dto: ResponseDto, + }; + const decorator = DocDefault(options); + + const mockSchema = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: 'test.message', + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(ResponseDto), + }, + }, + }; + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ApiExtraModels(options.dto as any), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + schema: mockSchema, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocResponse', () => { + it('should create a decorator with ApiResponse and ApiProduces', () => { + const options = { + httpStatus: HttpStatus.OK, + statusCode: HttpStatus.OK, + dto: ResponseDto, + }; + const decorator = DocResponse('test.message', options); + + const mockSchema = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: 'test.message', + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(ResponseDto), + }, + }, + }; + + const mockApplyDecorators = applyDecorators( + ApiProduces('application/json'), + ApiExtraModels(ResponseDto), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + schema: mockSchema, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with no options', () => { + const decorator = DocResponse('test.message'); + + const mockSchema = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: 'test.message', + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(ResponseDto), + }, + }, + }; + + const mockApplyDecorators = applyDecorators( + ApiProduces('application/json'), + ApiExtraModels(ResponseDto), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + schema: mockSchema, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocOneOf', () => { + it('should create a decorator with the correct ApiResponse schema', () => { + const documents = [ + { + messagePath: 'test.message1', + statusCode: HttpStatus.CREATED, + dto: ResponseDto, + }, + { + messagePath: 'test.message2', + statusCode: HttpStatus.BAD_REQUEST, + dto: ResponseDto, + }, + ]; + const httpStatus = HttpStatus.OK; + const decorator = DocOneOf(httpStatus, ...documents); + + const oneOfSchemas = documents.map(doc => { + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(doc.dto), + }, + }, + }; + return schema; + }); + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ...documents.map(doc => ApiExtraModels(doc.dto)), + ApiResponse({ + description: httpStatus.toString(), + status: httpStatus, + schema: { + oneOf: oneOfSchemas, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocAnyOf', () => { + it('should create a decorator with the correct ApiResponse schema', () => { + const documents = [ + { + messagePath: 'test.message1', + statusCode: HttpStatus.CREATED, + dto: ResponseDto, + }, + { + messagePath: 'test.message2', + statusCode: HttpStatus.BAD_REQUEST, + dto: ResponseDto, + }, + ]; + const httpStatus = HttpStatus.OK; + const decorator = DocAnyOf(httpStatus, ...documents); + + const anyOfSchemas = documents.map(doc => { + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(doc.dto), + }, + }, + }; + return schema; + }); + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ApiExtraModels(ResponseDto), + ApiResponse({ + description: httpStatus.toString(), + status: httpStatus, + schema: { + anyOf: anyOfSchemas, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with no document status code', () => { + const documents = [ + { + messagePath: 'test.message1', + dto: ResponseDto, + }, + { + messagePath: 'test.message2', + dto: ResponseDto, + }, + ]; + const httpStatus = HttpStatus.OK; + const decorator = DocAnyOf(httpStatus, ...(documents as any)); + + const anyOfSchemas = documents.map(doc => { + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(doc.dto), + }, + }, + }; + return schema; + }); + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ApiExtraModels(ResponseDto), + ApiResponse({ + description: httpStatus.toString(), + status: httpStatus, + schema: { + anyOf: anyOfSchemas, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocAllOf', () => { + it('should create a decorator with the correct ApiResponse schema', () => { + const documents = [ + { + messagePath: 'test.message1', + statusCode: HttpStatus.CREATED, + dto: ResponseDto, + }, + { + messagePath: 'test.message2', + statusCode: HttpStatus.BAD_REQUEST, + dto: ResponseDto, + }, + ]; + const httpStatus = HttpStatus.OK; + const decorator = DocAllOf(httpStatus, ...documents); + + const allOfSchemas = documents.map(doc => { + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(doc.dto), + }, + }, + }; + return schema; + }); + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ...documents.map(doc => ApiExtraModels(doc.dto)), + ApiResponse({ + description: httpStatus.toString(), + status: httpStatus, + schema: { + allOf: allOfSchemas, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with no documents status code', () => { + const documents = [ + { + messagePath: 'test.message1', + dto: ResponseDto, + }, + { + messagePath: 'test.message2', + dto: ResponseDto, + }, + ]; + const httpStatus = HttpStatus.OK; + const decorator = DocAllOf(httpStatus, ...(documents as any)); + + const allOfSchemas = documents.map(doc => { + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDto) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + $ref: getSchemaPath(doc.dto), + }, + }, + }; + return schema; + }); + + const mockApplyDecorators = applyDecorators( + ApiExtraModels(ResponseDto), + ...documents.map(doc => ApiExtraModels(doc.dto)), + ApiResponse({ + description: httpStatus.toString(), + status: httpStatus, + schema: { + allOf: allOfSchemas, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocDecorator', () => { + it('should create a decorator with the correct ApiOperation and ApiHeaders', () => { + const options = { + summary: 'Test Summary', + deprecated: false, + description: 'Test Description', + operation: 'TestOperation', + }; + + const decorator = Doc(options); + + const mockApplyDecorators = applyDecorators( + ApiOperation({ + summary: options.summary, + deprecated: options.deprecated, + description: options.description, + operationId: options.operation, + }), + ApiHeaders([ + { + name: 'x-custom-lang', + description: 'Custom language header', + required: false, + schema: { + default: ENUM_MESSAGE_LANGUAGE.EN, + example: ENUM_MESSAGE_LANGUAGE.EN, + type: 'string', + }, + }, + ]), + DocDefault({ + httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, + messagePath: 'http.serverError.internalServerError', + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, + }), + DocDefault({ + httpStatus: HttpStatus.REQUEST_TIMEOUT, + messagePath: 'http.serverError.requestTimeout', + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocRequestDecorator', () => { + it('should create a decorator with the correct ApiConsumes for FORM_DATA', () => { + const options: IDocRequestOptions = { + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.FORM_DATA, + }; + + const decorator = DocRequest(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('multipart/form-data'), + DocDefault({ + httpStatus: HttpStatus.UNPROCESSABLE_ENTITY, + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION, + messagePath: 'request.validation', + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiConsumes for TEXT', () => { + const options: IDocRequestOptions = { + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.TEXT, + }; + + const decorator = DocRequest(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('text/plain'), + DocDefault({ + httpStatus: HttpStatus.UNPROCESSABLE_ENTITY, + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION, + messagePath: 'request.validation', + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiConsumes for JSON', () => { + const options: IDocRequestOptions = { + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + }; + + const decorator = DocRequest(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('application/json'), + DocDefault({ + httpStatus: HttpStatus.UNPROCESSABLE_ENTITY, + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION, + messagePath: 'request.validation', + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiParam and ApiQuery', () => { + const options: IDocRequestOptions = { + params: [{ name: 'id', required: true, type: 'string' }], + queries: [{ name: 'filter', required: false, type: 'string' }], + }; + + const decorator = DocRequest(options); + + const mockApplyDecorators = applyDecorators( + ApiParam({ name: 'id', required: true, type: 'string' }), + ApiQuery({ name: 'filter', required: false, type: 'string' }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiBody', () => { + const options: IDocRequestOptions = { + dto: class Dto {}, + }; + + const decorator = DocRequest(options); + + const mockApplyDecorators = applyDecorators( + ApiBody({ type: options.dto }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocRequestFileDecorator', () => { + it('should create a decorator with the correct ApiConsumes and ApiParam', () => { + const options: IDocRequestFileOptions = { + params: [{ name: 'id', required: true, type: 'string' }], + }; + + const decorator = DocRequestFile(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('multipart/form-data'), + ApiParam({ name: 'id', required: true, type: 'string' }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiConsumes and ApiQuery', () => { + const options: IDocRequestFileOptions = { + queries: [{ name: 'filter', required: false, type: 'string' }], + }; + + const decorator = DocRequestFile(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('multipart/form-data'), + ApiQuery({ name: 'filter', required: false, type: 'string' }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiBody', () => { + const options: IDocRequestFileOptions = { + dto: class Dto {}, + }; + + const decorator = DocRequestFile(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('multipart/form-data'), + ApiBody({ type: options.dto }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with the correct ApiConsumes, ApiParam, ApiQuery, and ApiBody', () => { + const options: IDocRequestFileOptions = { + params: [{ name: 'id', required: true, type: 'string' }], + queries: [{ name: 'filter', required: false, type: 'string' }], + dto: class Dto {}, + }; + + const decorator = DocRequestFile(options); + + const mockApplyDecorators = applyDecorators( + ApiConsumes('multipart/form-data'), + ApiParam({ name: 'id', required: true, type: 'string' }), + ApiQuery({ name: 'filter', required: false, type: 'string' }), + ApiBody({ type: options.dto }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocGuardDecorator', () => { + it('should call DocOneOf with role forbidden error', () => { + const options: IDocGuardOptions = { + role: true, + }; + + const expectedOptions: IDocOfOptions[] = [ + { + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN, + messagePath: 'policy.error.roleForbidden', + }, + ]; + + const decorator = DocGuard(options); + + const mockApplyDecorators = applyDecorators( + DocOneOf(HttpStatus.FORBIDDEN, ...expectedOptions) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should call DocOneOf with policy forbidden error', () => { + const options: IDocGuardOptions = { + policy: true, + }; + + const expectedOptions: IDocOfOptions[] = [ + { + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN, + messagePath: 'policy.error.abilityForbidden', + }, + ]; + + const decorator = DocGuard(options); + + const mockApplyDecorators = applyDecorators( + DocOneOf(HttpStatus.FORBIDDEN, ...expectedOptions) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should call DocOneOf with both role and policy forbidden errors', () => { + const options: IDocGuardOptions = { + role: true, + policy: true, + }; + + const expectedOptions: IDocOfOptions[] = [ + { + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN, + messagePath: 'policy.error.roleForbidden', + }, + { + statusCode: ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN, + messagePath: 'policy.error.abilityForbidden', + }, + ]; + + const decorator = DocGuard(options); + const mockApplyDecorators = applyDecorators( + DocOneOf(HttpStatus.FORBIDDEN, ...expectedOptions) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should call DocOneOf with no options', () => { + const decorator = DocGuard(); + const mockApplyDecorators = applyDecorators( + DocOneOf(HttpStatus.FORBIDDEN, [] as any) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocAuthDecorator', () => { + it('should call DocOneOf with all options', () => { + const decorator = DocAuth({ + apple: true, + google: true, + jwtAccessToken: true, + jwtRefreshToken: true, + xApiKey: true, + }); + + const mockDocs = [ + ApiBearerAuth('refreshToken'), + ApiBearerAuth('accessToken'), + ApiBearerAuth('google'), + ApiBearerAuth('apple'), + ApiBearerAuth('xApiKey'), + ]; + + const mockOneOfUnauthorized = [ + { + messagePath: 'auth.error.refreshTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN, + }, + { + messagePath: 'auth.error.accessTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN, + }, + { + messagePath: 'auth.error.socialGoogle', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_GOOGLE, + }, + { + messagePath: 'auth.error.socialApple', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.SOCIAL_APPLE, + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED, + messagePath: 'apiKey.error.xApiKey.required', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND, + messagePath: 'apiKey.error.xApiKey.notFound', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED, + messagePath: 'apiKey.error.xApiKey.expired', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID, + messagePath: 'apiKey.error.xApiKey.invalid', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN, + messagePath: 'apiKey.error.xApiKey.forbidden', + }, + ]; + + const mockApplyDecorators = applyDecorators( + ...mockDocs, + DocOneOf(HttpStatus.UNAUTHORIZED, ...mockOneOfUnauthorized) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocErrorGroup', () => { + it('should return applyDecorators', () => { + const decorator = DocErrorGroup([]); + const mockApplyDecorators = applyDecorators(...[]); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocResponsePaging', () => { + it('should create a decorator with ApiResponse and ApiProduces', () => { + const options = { + httpStatus: HttpStatus.OK, + statusCode: HttpStatus.OK, + dto: ResponseDto, + }; + const decorator = DocResponsePaging('test.message', options); + const mockApplyDecorators = applyDecorators( + ApiProduces('application/json'), + ApiQuery({ + name: 'search', + required: false, + allowEmptyValue: true, + type: 'string', + description: + 'Search will base on _metadata.pagination._availableSearch with rule contains, and case insensitive', + }), + ApiQuery({ + name: 'perPage', + required: false, + allowEmptyValue: true, + example: 20, + type: 'number', + description: 'Data per page, max 100', + }), + ApiQuery({ + name: 'page', + required: false, + allowEmptyValue: true, + example: 1, + type: 'number', + description: 'page number, max 20', + }), + ApiQuery({ + name: 'orderBy', + required: false, + allowEmptyValue: true, + example: 'createdAt', + type: 'string', + description: + 'Order by base on _metadata.pagination.availableOrderBy', + }), + ApiQuery({ + name: 'orderDirection', + required: false, + allowEmptyValue: true, + example: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + enum: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + type: 'string', + description: + 'Order direction base on _metadata.pagination.availableOrderDirection', + }), + ApiExtraModels(ResponsePagingDto), + ApiExtraModels(options.dto as any), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + schema: { + allOf: [{ $ref: getSchemaPath(ResponsePagingDto) }], + properties: { + message: { + example: 'test.message', + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + type: 'array', + items: { + $ref: getSchemaPath(ResponseDto), + }, + }, + }, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with no options', () => { + const options = { + dto: ResponseDto, + }; + const decorator = DocResponsePaging('test.message', options); + const mockApplyDecorators = applyDecorators( + ApiProduces('application/json'), + ApiQuery({ + name: 'search', + required: false, + allowEmptyValue: true, + type: 'string', + description: + 'Search will base on _metadata.pagination._availableSearch with rule contains, and case insensitive', + }), + ApiQuery({ + name: 'perPage', + required: false, + allowEmptyValue: true, + example: 20, + type: 'number', + description: 'Data per page, max 100', + }), + ApiQuery({ + name: 'page', + required: false, + allowEmptyValue: true, + example: 1, + type: 'number', + description: 'page number, max 20', + }), + ApiQuery({ + name: 'orderBy', + required: false, + allowEmptyValue: true, + example: 'createdAt', + type: 'string', + description: + 'Order by base on _metadata.pagination.availableOrderBy', + }), + ApiQuery({ + name: 'orderDirection', + required: false, + allowEmptyValue: true, + example: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + enum: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + type: 'string', + description: + 'Order direction base on _metadata.pagination.availableOrderDirection', + }), + ApiExtraModels(ResponsePagingDto), + ApiExtraModels(options.dto as any), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + schema: { + allOf: [{ $ref: getSchemaPath(ResponsePagingDto) }], + properties: { + message: { + example: 'test.message', + }, + statusCode: { + type: 'number', + example: HttpStatus.OK, + }, + data: { + type: 'array', + items: { + $ref: getSchemaPath(ResponseDto), + }, + }, + }, + }, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); + + describe('DocResponseFile', () => { + it('should create a decorator with ApiResponse and ApiProduces', () => { + const options = { + httpStatus: HttpStatus.OK, + fileType: ENUM_FILE_MIME.CSV, + }; + + const decorator = DocResponseFile(options); + + const mockApplyDecorators = applyDecorators( + ApiProduces(ENUM_FILE_MIME.CSV), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + + it('should create a decorator with no options', () => { + const decorator = DocResponseFile(); + + const mockApplyDecorators = applyDecorators( + ApiProduces(ENUM_FILE_MIME.CSV), + ApiResponse({ + description: HttpStatus.OK.toString(), + status: HttpStatus.OK, + }) + ); + + expect(decorator.toString()).toEqual( + mockApplyDecorators.toString() + ); + }); + }); +}); diff --git a/test/common/file/decorators/file.decorator.spec.ts b/test/common/file/decorators/file.decorator.spec.ts new file mode 100644 index 000000000..ae6ca7b86 --- /dev/null +++ b/test/common/file/decorators/file.decorator.spec.ts @@ -0,0 +1,201 @@ +import { createMock } from '@golevelup/ts-jest'; +import { + applyDecorators, + ExecutionContext, + UseInterceptors, +} from '@nestjs/common'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { + FileFieldsInterceptor, + FileInterceptor, + FilesInterceptor, +} from '@nestjs/platform-express'; +import { FILE_SIZE_IN_BYTES } from 'src/common/file/constants/file.constant'; +import { + FileUploadSingle, + FileUploadMultiple, + FileUploadMultipleFields, + FilePartNumber, +} from 'src/common/file/decorators/file.decorator'; + +/* eslint-disable */ +function getParamDecoratorFactory(decorator: any) { + class Test { + public test(@decorator() value) {} + } + + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test'); + return args[Object.keys(args)[0]].factory; +} +/* eslint-enable */ + +describe('File Decorators', () => { + describe('FileUploadSingle', () => { + it('should apply the FileInterceptor with correct options', () => { + const decorator = FileUploadSingle({ + field: 'testFile', + fileSize: 1024, + }); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FileInterceptor('testFile', { + limits: { + fileSize: 1024, + files: 1, + }, + }) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + + it('should apply the FileInterceptor with no options', () => { + const decorator = FileUploadSingle(); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FileInterceptor('file', { + limits: { + fileSize: FILE_SIZE_IN_BYTES, + files: 1, + }, + }) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + }); + + describe('FileUploadMultiple', () => { + it('should apply the FilesInterceptor with correct options', () => { + const decorator = FileUploadMultiple({ + field: 'testFiles', + maxFiles: 5, + fileSize: 2048, + }); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FilesInterceptor('testFiles', 5, { + limits: { + fileSize: 2048, + }, + }) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + + it('should apply the FilesInterceptor with no options', () => { + const decorator = FileUploadMultiple(); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FilesInterceptor('files', 2, { + limits: { + fileSize: FILE_SIZE_IN_BYTES, + }, + }) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + }); + + describe('FileUploadMultipleFields', () => { + it('should apply the FileFieldsInterceptor with correct options', () => { + const fields = [ + { field: 'file1', maxFiles: 1 }, + { field: 'file2', maxFiles: 2 }, + ]; + const decorator = FileUploadMultipleFields(fields, { + fileSize: 4096, + }); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FileFieldsInterceptor( + [ + { name: 'file1', maxCount: 1 }, + { name: 'file2', maxCount: 2 }, + ], + { + limits: { + fileSize: 4096, + }, + } + ) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + + it('should apply the FileFieldsInterceptor with no options', () => { + const fields = [ + { field: 'file1', maxFiles: 1 }, + { field: 'file2', maxFiles: 2 }, + ]; + const decorator = FileUploadMultipleFields(fields); + + const mockApplyDecorator = applyDecorators( + UseInterceptors( + FileFieldsInterceptor( + [ + { name: 'file1', maxCount: 1 }, + { name: 'file2', maxCount: 2 }, + ], + { + limits: { + fileSize: FILE_SIZE_IN_BYTES, + }, + } + ) + ) + ); + + expect(decorator.toString()).toEqual(mockApplyDecorator.toString()); + }); + }); + + describe('FilePartNumber', () => { + it('should return the correct part number from the headers', () => { + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + headers: { + 'x-part-number': '5', + }, + }), + }), + }); + + const decorator = getParamDecoratorFactory(FilePartNumber); + + const result = decorator(null, executionContext); + expect(result).toBeTruthy(); + expect(result).toBe(5); + }); + + it('should return 0 if part number header is not present', () => { + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + headers: {}, + }), + }), + }); + + const decorator = getParamDecoratorFactory(FilePartNumber); + + const result = decorator(null, executionContext); + expect(result).toBeFalsy(); + expect(result).toBe(undefined); + }); + }); +}); diff --git a/test/common/file/dtos/file.multiple.dto.spec.ts b/test/common/file/dtos/file.multiple.dto.spec.ts new file mode 100644 index 000000000..e5a0bb58c --- /dev/null +++ b/test/common/file/dtos/file.multiple.dto.spec.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata'; +import { plainToInstance } from 'class-transformer'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { FileMultipleDto } from 'src/common/file/dtos/file.multiple.dto'; + +describe('FileMultipleDto', () => { + it('should create a valid FileMultipleDto object', () => { + const mockFileSingleDto: FileMultipleDto = { + files: [{}] as IFile[], + }; + + const dto = plainToInstance(FileMultipleDto, mockFileSingleDto); + + expect(dto).toBeInstanceOf(FileMultipleDto); + }); +}); diff --git a/test/common/file/dtos/file.single.dto.spec.ts b/test/common/file/dtos/file.single.dto.spec.ts new file mode 100644 index 000000000..dd605e204 --- /dev/null +++ b/test/common/file/dtos/file.single.dto.spec.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata'; +import { plainToInstance } from 'class-transformer'; +import { FileSingleDto } from 'src/common/file/dtos/file.single.dto'; +import { IFile } from 'src/common/file/interfaces/file.interface'; + +describe('FileSingleDto', () => { + it('should create a valid FileSingleDto object', () => { + const mockFileSingleDto: FileSingleDto = { + file: {} as IFile, + }; + + const dto = plainToInstance(FileSingleDto, mockFileSingleDto); + + expect(dto).toBeInstanceOf(FileSingleDto); + }); +}); diff --git a/test/common/file/exceptions/file.import.exception.spec.ts b/test/common/file/exceptions/file.import.exception.spec.ts new file mode 100644 index 000000000..fbd62d050 --- /dev/null +++ b/test/common/file/exceptions/file.import.exception.spec.ts @@ -0,0 +1,33 @@ +import { HttpStatus } from '@nestjs/common'; +import { FileImportException } from 'src/common/file/exceptions/file.import.exception'; +import { IMessageValidationImportErrorParam } from 'src/common/message/interfaces/message.interface'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; + +describe('FileImportException', () => { + it('should create a HttpException with the correct status code and message', () => { + const errors: IMessageValidationImportErrorParam[] = [ + { + row: 1, + sheetName: 'Sheet1', + errors: [ + { + property: 'field1', + constraints: { + isNotEmpty: 'field1 should not be empty', + }, + }, + ], + }, + ]; + + const exception = new FileImportException(errors); + + expect(exception).toBeInstanceOf(FileImportException); + expect(exception.httpStatus).toBe(HttpStatus.UNPROCESSABLE_ENTITY); + expect(exception.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION + ); + expect(exception.message).toEqual('file.error.validationDto'); + expect(exception.errors).toEqual(errors); + }); +}); diff --git a/test/common/file/pipes/file.excel-extract.pipe.spec.ts b/test/common/file/pipes/file.excel-extract.pipe.spec.ts new file mode 100644 index 000000000..60653f2a2 --- /dev/null +++ b/test/common/file/pipes/file.excel-extract.pipe.spec.ts @@ -0,0 +1,118 @@ +import { UnsupportedMediaTypeException } from '@nestjs/common'; +import { + ENUM_FILE_MIME, + ENUM_FILE_MIME_EXCEL, +} from 'src/common/file/enums/file.enum'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; +import { IFile, IFileRows } from 'src/common/file/interfaces/file.interface'; +import { FileExcelParsePipe } from 'src/common/file/pipes/file.excel-extract.pipe'; +import { FileService } from 'src/common/file/services/file.service'; + +describe('FileExcelParsePipe', () => { + let pipe: FileExcelParsePipe; + let fileService: FileService; + + beforeEach(() => { + fileService = { + readCsv: jest.fn(), + readExcel: jest.fn(), + } as any; + pipe = new FileExcelParsePipe(fileService); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + it('should pass through valid CSV file', async () => { + const file: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME.CSV, + // other IFile properties... + } as any; + const expectedParse: IFileRows = { data: [], sheetName: 'Sheet1' }; + (fileService.readCsv as jest.Mock).mockReturnValue(expectedParse); + + const result = await pipe.transform(file); + expect(result).toEqual([expectedParse]); + }); + + it('should invalid CSV file', async () => { + const result = await pipe.transform(null); + expect(result).toEqual(undefined); + }); + + it('should pass through valid Excel file', async () => { + const file: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME_EXCEL.XLSX, + // other IFile properties... + } as any; + const expectedParse: IFileRows[] = [ + { data: [], sheetName: 'Sheet1' }, + ]; + (fileService.readExcel as jest.Mock).mockReturnValue(expectedParse); + + const result = await pipe.transform(file); + expect(result).toEqual(expectedParse); + }); + + it('should throw UnsupportedMediaTypeException for invalid file type', async () => { + const file: IFile = { + buffer: Buffer.from('test'), + mimetype: 'application/pdf', + // other IFile properties... + } as any; + + await expect(pipe.transform(file)).rejects.toThrow( + new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_INVALID, + message: 'file.error.mimeInvalid', + }) + ); + }); + + it('should validate supported file types correctly', async () => { + const csvFile: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME.CSV, + } as any; + const xlsxFile: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME_EXCEL.XLSX, + } as any; + + await expect(pipe.validate(csvFile)).resolves.toBeUndefined(); + await expect(pipe.validate(xlsxFile)).resolves.toBeUndefined(); + }); + + it('should call parseCsv for CSV files', async () => { + const file: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME.CSV, + // other IFile properties... + } as any; + const expectedParse: IFileRows = { data: [], sheetName: 'Sheet1' }; + (fileService.readCsv as jest.Mock).mockReturnValue(expectedParse); + + const result = pipe.parse(file); + expect(result).toEqual([expectedParse]); + expect(fileService.readCsv).toHaveBeenCalledWith(file.buffer); + }); + + it('should call parseExcel for Excel files', async () => { + const file: IFile = { + buffer: Buffer.from('test'), + mimetype: ENUM_FILE_MIME_EXCEL.XLSX, + // other IFile properties... + } as any; + const expectedParse: IFileRows[] = [ + { data: [], sheetName: 'Sheet1' }, + ]; + (fileService.readExcel as jest.Mock).mockReturnValue(expectedParse); + + const result = pipe.parse(file); + expect(result).toEqual(expectedParse); + expect(fileService.readExcel).toHaveBeenCalledWith(file.buffer); + }); +}); diff --git a/test/common/file/pipes/file.excel-validation.pipe.spec.ts b/test/common/file/pipes/file.excel-validation.pipe.spec.ts new file mode 100644 index 000000000..c044ce44c --- /dev/null +++ b/test/common/file/pipes/file.excel-validation.pipe.spec.ts @@ -0,0 +1,57 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { IsString, ValidationError } from 'class-validator'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; +import { FileImportException } from 'src/common/file/exceptions/file.import.exception'; +import { FileExcelValidationPipe } from 'src/common/file/pipes/file.excel-validation.pipe'; + +class MockDto { + @IsString() + field: string; +} + +describe('FileExcelValidationPipe', () => { + let pipe: FileExcelValidationPipe; + + beforeEach(async () => { + pipe = new FileExcelValidationPipe(MockDto); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + it('should pass through valid file rows', async () => { + const fileRows = [ + { sheetName: 'Sheet1', data: { field: 'value' } as any }, + ]; + const result = await pipe.transform(fileRows as any); + expect(result).toEqual(fileRows); + }); + + it('should throw UnprocessableEntityException for null value', async () => { + await expect(pipe.transform(null)); + }); + + it('should throw UnprocessableEntityException for empty array', async () => { + await expect(pipe.transform([])).rejects.toThrow( + new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED_EXTRACT_FIRST, + message: 'file.error.requiredParseFirst', + }) + ); + }); + + it('should throw FileImportException for invalid data', async () => { + const fileRows = [{ sheetName: 'Sheet1', data: { field: null } }]; + + await expect(pipe.transform(fileRows as any)).rejects.toThrow( + new FileImportException([ + { + row: 0, + sheetName: 'Sheet1', + errors: [new ValidationError()], + }, + ]) + ); + }); +}); diff --git a/test/common/file/pipes/file.required.pipe.spec.ts b/test/common/file/pipes/file.required.pipe.spec.ts new file mode 100644 index 000000000..47af46a7b --- /dev/null +++ b/test/common/file/pipes/file.required.pipe.spec.ts @@ -0,0 +1,69 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; +import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; + +describe('FileRequiredPipe', () => { + let pipe: FileRequiredPipe; + + beforeEach(() => { + pipe = new FileRequiredPipe(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + it('should pass through valid file', async () => { + const file = { originalname: 'test.jpg', buffer: Buffer.from('') }; + const result = await pipe.transform(file as any); + expect(result).toBe(file); + }); + + it('should throw UnprocessableEntityException for null value', async () => { + await expect(pipe.transform(undefined)).rejects.toThrow( + new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED, + message: 'file.error.required', + }) + ); + }); + + it('should throw UnprocessableEntityException for empty object', async () => { + await expect(pipe.transform({} as any)).rejects.toThrow( + new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED, + message: 'file.error.required', + }) + ); + }); + + it('should throw UnprocessableEntityException for empty array', async () => { + await expect(pipe.transform([] as any)).rejects.toThrow( + new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED, + message: 'file.error.required', + }) + ); + }); + + it('should handle field property', async () => { + pipe = new FileRequiredPipe('file'); + const value = { + file: { originalname: 'test.jpg', buffer: Buffer.from('') }, + }; + const result = await pipe.transform(value as any); + expect(result).toBe(value); + }); + + it('should throw UnprocessableEntityException for invalid field property', async () => { + pipe = new FileRequiredPipe('file'); + const value = { file: null }; + + await expect(pipe.transform(value as any)).rejects.toThrow( + new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.REQUIRED, + message: 'file.error.required', + }) + ); + }); +}); diff --git a/test/common/file/pipes/file.type.pipe.spec.ts b/test/common/file/pipes/file.type.pipe.spec.ts new file mode 100644 index 000000000..c74ea02fb --- /dev/null +++ b/test/common/file/pipes/file.type.pipe.spec.ts @@ -0,0 +1,76 @@ +import { UnsupportedMediaTypeException } from '@nestjs/common'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/enums/file.status-code.enum'; +import { FileTypePipe } from 'src/common/file/pipes/file.type.pipe'; + +describe('FileTypePipe', () => { + let pipe: FileTypePipe; + + beforeEach(() => { + pipe = new FileTypePipe([ENUM_FILE_MIME.JPG, ENUM_FILE_MIME.PNG]); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + it('should pass through valid MIME type', async () => { + const file = { mimetype: ENUM_FILE_MIME.JPG }; + const result = await pipe.transform(file); + expect(result).toBe(file); + }); + + it('should throw UnsupportedMediaTypeException for invalid MIME type', async () => { + const file = { mimetype: 'application/pdf' }; + + await expect(pipe.transform(file)).rejects.toThrow( + new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_INVALID, + message: 'file.error.mimeInvalid', + }) + ); + }); + + it('should handle array of files', async () => { + const files = [ + { mimetype: ENUM_FILE_MIME.JPG }, + { mimetype: ENUM_FILE_MIME.PNG }, + ]; + const result = await pipe.transform(files); + expect(result).toBe(files); + }); + + it('should handle empty value', async () => { + const result = await pipe.transform(null); + expect(result).toBeNull(); + }); + + it('should handle empty field in object', async () => { + const result = await pipe.transform({}); + expect(result).toEqual({}); + }); + + it('should handle empty array', async () => { + const result = await pipe.transform([]); + expect(result).toEqual([]); + }); + + it('should handle field property', async () => { + pipe = new FileTypePipe([ENUM_FILE_MIME.JPG], 'file'); + const value = { file: { mimetype: ENUM_FILE_MIME.JPG } }; + const result = await pipe.transform(value); + expect(result).toBe(value); + }); + + it('should handle invalid MIME type in field property', async () => { + pipe = new FileTypePipe([ENUM_FILE_MIME.JPG], 'file'); + const value = { file: { mimetype: 'application/pdf' } }; + + await expect(pipe.transform(value)).rejects.toThrow( + new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.MIME_INVALID, + message: 'file.error.mimeInvalid', + }) + ); + }); +}); diff --git a/test/common/file/services/file.service.spec.ts b/test/common/file/services/file.service.spec.ts new file mode 100644 index 000000000..8bd74a454 --- /dev/null +++ b/test/common/file/services/file.service.spec.ts @@ -0,0 +1,73 @@ +import { Test } from '@nestjs/testing'; +import { FileService } from 'src/common/file/services/file.service'; +import { Buffer } from 'buffer'; + +describe('FileService', () => { + let service: FileService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + FileService, + { + provide: Buffer, + useValue: { + from: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(FileService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('writeCsv', () => { + it('should write CSV', () => { + expect(service.writeCsv({ data: ['data'] })).toBeInstanceOf(Buffer); + }); + }); + + describe('writeCsvFromArray', () => { + it('should write CSV from array', () => { + expect(service.writeCsvFromArray([['data']])).toBeInstanceOf( + Buffer + ); + }); + }); + + describe('writeExcel', () => { + it('should write excel', () => { + expect(service.writeExcel([{ data: ['data'] }])).toBeInstanceOf( + Buffer + ); + }); + }); + + describe('writeExcelFromArray', () => { + it('should write Excel from array', () => { + expect( + service.writeExcelFromArray([['test', 'data']]) + ).toBeInstanceOf(Buffer); + }); + }); + + describe('readCsv', () => { + it('should read CSV', () => { + const result = service.readCsv(Buffer.from('mocked CSV', 'utf8')); + expect(result).toBeInstanceOf(Object); + }); + }); + + describe('readExcel', () => { + it('should read Excel', () => { + const result = service.readExcel( + Buffer.from('mocked Excel', 'utf8') + ); + expect(result).toBeInstanceOf(Object); + }); + }); +}); diff --git a/test/common/helper/services/helper.array.service.spec.ts b/test/common/helper/services/helper.array.service.spec.ts new file mode 100644 index 000000000..9186f429a --- /dev/null +++ b/test/common/helper/services/helper.array.service.spec.ts @@ -0,0 +1,238 @@ +import { Test } from '@nestjs/testing'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import _ from 'lodash'; + +jest.mock('lodash', () => ({ + take: jest.fn(), + takeRight: jest.fn(), + difference: jest.fn(), + intersection: jest.fn(), + concat: jest.fn(), + union: jest.fn(), + uniq: jest.fn(), + shuffle: jest.fn(), + isEqual: jest.fn(), + chunk: jest.fn(), +})); + +describe('HelperArrayService', () => { + let service: HelperArrayService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [HelperArrayService], + }).compile(); + + service = moduleRef.get(HelperArrayService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getFromLeft', () => { + it('should return left part of array', () => { + const input = [1, 2, 3, 4, 5]; + const length = 3; + const result = [1, 2, 3]; + + jest.spyOn(_, 'take').mockImplementation(() => result); + expect(service.getFromLeft(input, length)).toEqual(result); + expect(_.take(input, length)).toEqual(result); + }); + + it('should return entire array if length is greater than array length', () => { + const array = [1, 2, 3]; + const length = 5; + + jest.spyOn(_, 'take').mockImplementation(() => array); + expect(service.getFromLeft(array, length)).toEqual(array); + }); + + it('should return empty array if length is zero', () => { + const array = [1, 2, 3]; + const length = 0; + const result = []; + + jest.spyOn(_, 'take').mockImplementation(() => result); + expect(service.getFromLeft(array, length)).toEqual(result); + }); + }); + + describe('getFromRight', () => { + it('should return right part of array', () => { + const input = [1, 2, 3, 4, 5]; + const length = 2; + const result = [4, 5]; + + jest.spyOn(_, 'takeRight').mockImplementation(() => result); + expect(service.getFromRight(input, length)).toEqual(result); + }); + }); + + describe('getDifference', () => { + it('should return difference of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = [1, 2]; + + jest.spyOn(_, 'difference').mockImplementation(() => result); + expect(service.getDifference(a1, a2)).toEqual(result); + }); + }); + + describe('getIntersection', () => { + it('should return intersection of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = [3]; + + jest.spyOn(_, 'intersection').mockImplementation(() => result); + expect(service.getIntersection(a1, a2)).toEqual(result); + }); + }); + + describe('concat', () => { + it('should return concat of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = [1, 2, 3, 3, 4, 5]; + + jest.spyOn(_, 'concat').mockImplementation(() => result); + expect(service.concat(a1, a2)).toEqual(result); + }); + }); + + describe('concatUnique', () => { + it('should return concat unique of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = [1, 2, 3, 4, 5]; + + jest.spyOn(_, 'union').mockImplementation(() => result); + expect(service.concatUnique(a1, a2)).toEqual(result); + }); + }); + + describe('unique', () => { + it('should return unique of array', () => { + const a1 = [1, 2, 3, 3, 2, 1]; + const result = [1, 2, 3]; + + jest.spyOn(_, 'uniq').mockImplementation(() => result); + expect(service.unique(a1)).toEqual(result); + }); + }); + + describe('shuffle', () => { + it('should return shuffle of array', () => { + const a1 = [1, 2, 3]; + const result = [2, 1, 3]; + + jest.spyOn(_, 'shuffle').mockImplementation(() => result); + expect(service.shuffle(a1)).toEqual(result); + }); + }); + + describe('equals true', () => { + it('should return equals true of array', () => { + const a1 = [1, 2, 3]; + const a2 = [1, 2, 3]; + const result = true; + + jest.spyOn(_, 'isEqual').mockImplementation(() => result); + expect(service.equals(a1, a2)).toBe(result); + }); + }); + + describe('equals false', () => { + it('should return equals false of array', () => { + const a1 = [1, 2, 3]; + const a2 = [2, 3, 4]; + const result = false; + + jest.spyOn(_, 'isEqual').mockImplementation(() => result); + expect(service.equals(a1, a2)).toBe(result); + }); + }); + + describe('notEquals true', () => { + it('should return not equals true of array', () => { + const a1 = [1, 2, 3]; + const a2 = [2, 3, 4]; + const result = true; + + jest.spyOn(_, 'isEqual').mockImplementation(() => false); + expect(service.notEquals(a1, a2)).toBe(result); + }); + }); + + describe('notEquals false', () => { + it('should return not equals false of array', () => { + const a1 = [1, 2, 3]; + const a2 = [1, 2, 3]; + const result = false; + + jest.spyOn(_, 'isEqual').mockImplementation(() => true); + expect(service.notEquals(a1, a2)).toBe(result); + }); + }); + + describe('in true', () => { + it('should return in true of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = true; + + jest.spyOn(_, 'intersection').mockImplementation(() => [3]); + expect(service.in(a1, a2)).toBe(result); + }); + }); + + describe('in false', () => { + it('should return in false of array', () => { + const a1 = [1, 2, 3]; + const a2 = [4, 5]; + const result = false; + + jest.spyOn(_, 'intersection').mockImplementation(() => []); + expect(service.in(a1, a2)).toBe(result); + }); + }); + + describe('notIn false', () => { + it('should return not in false of array', () => { + const a1 = [1, 2, 3]; + const a2 = [3, 4, 5]; + const result = false; + + jest.spyOn(_, 'intersection').mockImplementation(() => [3]); + expect(service.notIn(a1, a2)).toBe(result); + }); + }); + + describe('notIn true', () => { + it('should return not in true of array', () => { + const a1 = [1, 2, 3]; + const a2 = [4, 5]; + const result = true; + + jest.spyOn(_, 'intersection').mockImplementation(() => []); + expect(service.notIn(a1, a2)).toBe(result); + }); + }); + + describe('chunk', () => { + it('should chunk array', () => { + const input = [1, 2, 3]; + const result = [[1, 2, 3]]; + + jest.spyOn(_, 'chunk').mockImplementation(() => result); + expect(service.chunk(input, 3)).toBe(result); + }); + }); +}); diff --git a/test/common/helper/services/helper.date.service.spec.ts b/test/common/helper/services/helper.date.service.spec.ts new file mode 100644 index 000000000..325842748 --- /dev/null +++ b/test/common/helper/services/helper.date.service.spec.ts @@ -0,0 +1,615 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ConfigService } from '@nestjs/config'; +import moment from 'moment-timezone'; +import { + IHelperDateCreateOptions, + IHelperDateDiffOptions, + IHelperDateFormatOptions, + IHelperDateForwardOptions, + IHelperDateRoundDownOptions, + IHelperDateSetTimeOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { ENUM_HELPER_DATE_DIFF } from 'src/common/helper/enums/helper.enum'; + +class MockConfigService { + get(): string { + return 'Asia/Jakarta'; + } +} + +describe('HelperDateService', () => { + let service: HelperDateService; + let module: TestingModule; + let defTz: string; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + HelperDateService, + { + provide: ConfigService, + useClass: MockConfigService, + }, + ], + }).compile(); + + service = module.get(HelperDateService); + defTz = 'Asia/Jakarta'; + }); + + afterAll(async () => { + await module.close(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('calculateAge', () => { + it('should calculate age correctly with birth date', () => { + const dateOfBirth = new Date('1990-01-01'); + const age = service.calculateAge(dateOfBirth); + expect(age).toBeGreaterThan(0); + }); + + it('should calculate age correctly with year', () => { + const dateOfBirth = new Date('1990-01-01'); + const age = service.calculateAge(dateOfBirth, 1996); + expect(age).toBeGreaterThan(0); + }); + }); + + describe('diff', () => { + it('should return diff as days', () => { + const dateOne = new Date('1990-01-01'); + const dateTwo = new Date('1990-01-02'); + + const diff = service.diff(dateOne, dateTwo); + expect(diff).toEqual(1); + }); + + it('should return diff as milliseconds', () => { + const dateOne = new Date('1990-01-01'); + const dateTwo = new Date('1990-01-02'); + const opts: IHelperDateDiffOptions = { + format: ENUM_HELPER_DATE_DIFF.MILIS, + }; + + const diff = service.diff(dateOne, dateTwo, opts); + expect(diff).toBeGreaterThan(0); + }); + + it('should return diff as seconds', () => { + const dateOne = new Date('1990-01-01'); + const dateTwo = new Date('1990-01-02'); + const opts: IHelperDateDiffOptions = { + format: ENUM_HELPER_DATE_DIFF.SECONDS, + }; + + const diff = service.diff(dateOne, dateTwo, opts); + expect(diff).toBeGreaterThan(0); + }); + + it('should return diff as hours', () => { + const dateOne = new Date('1990-01-01'); + const dateTwo = new Date('1990-01-02'); + const opts: IHelperDateDiffOptions = { + format: ENUM_HELPER_DATE_DIFF.HOURS, + }; + + const diff = service.diff(dateOne, dateTwo, opts); + expect(diff).toBeGreaterThan(0); + }); + + it('should return diff as minutes', () => { + const dateOne = new Date('1990-01-01'); + const dateTwo = new Date('1990-01-02'); + const opts: IHelperDateDiffOptions = { + format: ENUM_HELPER_DATE_DIFF.MINUTES, + }; + + const diff = service.diff(dateOne, dateTwo, opts); + expect(diff).toBeGreaterThan(0); + }); + }); + + describe('check', () => { + it('should check format date correctly', () => { + const isValid = service.check(new Date('2023-07-01')); + expect(isValid).toBe(true); + }); + }); + + describe('checkIso', () => { + it('should check iso date correctly', () => { + const date = new Date().toISOString(); + expect(service.checkIso(date)).toBe(true); + }); + }); + + describe('checkTimestamp', () => { + it('should check timestamp date correctly', () => { + const date = new Date().getTime(); + expect(service.checkTimestamp(date)).toBe(true); + }); + }); + + describe('create', () => { + it('should create date successfully', () => { + const date = new Date(); + + expect(service.create(date)).toEqual(moment(date).toDate()); + }); + + it('should create undefined date successfully', () => { + const date = service.create(); + + expect(date).toBeDefined(); + expect(date).toBeInstanceOf(Date); + }); + + it('should create date start of day successfully', () => { + const date = new Date(); + const opts: IHelperDateCreateOptions = { + startOfDay: true, + }; + + expect(service.create(date, opts)).toEqual( + moment(date).tz(defTz).startOf('day').toDate() + ); + }); + }); + + describe('createTimestamp', () => { + it('should create timestamp successfully', () => { + const date = new Date(); + + expect(service.createTimestamp(date)).toEqual( + moment(date).valueOf() + ); + }); + + it('should create undefined timestamp successfully', () => { + const timestamp = service.createTimestamp(); + + expect(timestamp).toBeDefined(); + expect(typeof timestamp).toEqual('number'); + }); + + it('should create timestamp start of day successfully', () => { + const date = new Date(); + const opts: IHelperDateCreateOptions = { + startOfDay: true, + }; + + expect(service.createTimestamp(date, opts)).toEqual( + moment(date).tz(defTz).startOf('day').valueOf() + ); + }); + }); + + describe('format', () => { + it('should format successfully', () => { + const date = new Date('2023-07-01'); + const result = date.toISOString().split('T')[0]; + const formattedDate = service.format(date); + + expect(formattedDate).toEqual(result); + }); + + it('should format with locale successfully', () => { + const date = new Date('2023-07-01'); + const opts: IHelperDateFormatOptions = { + locale: 'en', + }; + + const result = date.toISOString().split('T')[0]; + const formattedDate = service.format(date, opts); + + expect(formattedDate).toEqual(result); + }); + }); + + describe('formatIsoDurationFromMinutes', () => { + it('should format iso duration from minutes successfully', () => { + expect(service.formatIsoDurationFromMinutes(60)).toEqual('PT1H'); + }); + }); + + describe('formatIsoDurationFromHours', () => { + it('should format iso duration from hours successfully', () => { + expect(service.formatIsoDurationFromHours(60)).toEqual('PT60H'); + }); + }); + + describe('formatIsoDurationFromDays', () => { + it('should format iso duration from days successfully', () => { + expect(service.formatIsoDurationFromDays(60)).toEqual('P60D'); + }); + }); + + describe('forwardInSeconds', () => { + it('should forward in seconds successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInSeconds(60).valueOf()).toBeGreaterThan( + now.unix() + ); + }); + + it('should forward in seconds with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect( + service.forwardInSeconds(60, opts).valueOf() + ).toBeGreaterThan(now.unix()); + }); + }); + + describe('backwardInSeconds', () => { + it('should backward date in seconds successfully', () => { + const date = new Date(); + + expect(service.backwardInSeconds(60).valueOf()).toBeLessThan( + date.valueOf() + ); + }); + + it('should backward date in seconds with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInSeconds(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('forwardInMinutes', () => { + it('should forward minutes successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInMinutes(60).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + + it('should forward minutes with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect( + service.forwardInMinutes(60, opts).valueOf() + ).toBeGreaterThan(now.toDate().valueOf()); + }); + }); + + describe('backwardInMinutes', () => { + it('should backward date in minutes successfully', () => { + const now = moment().tz(defTz); + + expect(service.backwardInMinutes(60).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + + it('should backward date in minutes with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInMinutes(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('forwardInHours', () => { + it('should forward hours successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInHours(60).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + + it('should forward hours with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.forwardInHours(60, opts).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + }); + + describe('backwardInHours', () => { + it('should backward date in hours successfully', () => { + const now = moment().tz(defTz); + + expect(service.backwardInHours(60).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + + it('should backward date in hours with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInHours(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('forwardInDays', () => { + it('should forward days successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInDays(60).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + + it('should forward days with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.forwardInDays(60, opts).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + }); + + describe('backwardInDays', () => { + it('should backward date in days successfully', () => { + const now = moment().tz(defTz); + + expect(service.backwardInDays(60).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + + it('should backward date in days with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInDays(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('forwardInMonths', () => { + it('should forward months successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInMonths(60).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + + it('should forward months with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.forwardInMonths(60, opts).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + }); + + describe('backwardInMonths', () => { + it('should backward date in months successfully', () => { + const now = moment().tz(defTz); + + expect(service.backwardInMonths(60).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + + it('should backward date in months with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInMonths(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('forwardInYears', () => { + it('should forward years successfully', () => { + const now = moment().tz(defTz); + + expect(service.forwardInYears(60).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + + it('should forward years with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.forwardInYears(60, opts).valueOf()).toBeGreaterThan( + now.toDate().valueOf() + ); + }); + }); + + describe('backwardInYears', () => { + it('should backward date in years successfully', () => { + const now = moment().tz(defTz); + + expect(service.backwardInYears(60).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + + it('should backward date in years with options successfully', () => { + const now = moment().tz(defTz); + const opts: IHelperDateForwardOptions = { + fromDate: now.toDate(), + }; + + expect(service.backwardInYears(60, opts).valueOf()).toBeLessThan( + now.toDate().valueOf() + ); + }); + }); + + describe('endOfMonth', () => { + it('should change date to end of month successfully', () => { + const now = new Date('2020-01-01'); + + expect(service.endOfMonth(now).valueOf()).toBeGreaterThan( + now.valueOf() + ); + }); + }); + + describe('startOfMonth', () => { + it('should change date to start of month successfully', () => { + const now = new Date('2020-01-30'); + + expect(service.startOfMonth(now).valueOf()).toBeLessThan( + now.valueOf() + ); + }); + }); + + describe('endOfYear', () => { + it('should change date to end of year successfully', () => { + const date = new Date('2024-01-01'); + + expect(service.endOfYear(date).valueOf()).toBeGreaterThan( + date.valueOf() + ); + }); + }); + + describe('startOfYear', () => { + it('should change date to start of year successfully', () => { + const now = new Date('2020-12-30'); + + expect(service.startOfYear(now).valueOf()).toBeLessThan( + now.valueOf() + ); + }); + }); + + describe('endOfDay', () => { + it('should change date to end of day successfully', () => { + const date = new Date(); + + expect(service.endOfDay(date).valueOf()).toBeGreaterThan( + date.valueOf() + ); + }); + }); + + describe('startOfDay', () => { + it('should change date to start of day successfully', () => { + const now = new Date(); + + expect(service.startOfDay(now).valueOf()).toBeLessThan( + now.valueOf() + ); + }); + }); + + describe('setTime', () => { + it('should set time successfully', () => { + const date = new Date(); + const opts: IHelperDateSetTimeOptions = { + hour: 10, + minute: 10, + second: 10, + millisecond: 10, + }; + + const result = moment(date) + .tz(defTz) + .set({ + hour: opts.hour, + minute: opts.minute, + second: opts.second, + millisecond: opts.millisecond, + }) + .toDate(); + + expect(service.setTime(date, opts)).toEqual(result); + }); + }); + + describe('addTime', () => { + it('should add time successfully', () => { + const date = new Date(); + const opts: IHelperDateSetTimeOptions = { + hour: 10, + minute: 10, + second: 10, + millisecond: 10, + }; + + expect(service.addTime(date, opts).valueOf()).toBeGreaterThan( + date.valueOf() + ); + }); + }); + + describe('subtractTime', () => { + it('should subtract time successfully', () => { + const date = new Date(); + const opts: IHelperDateSetTimeOptions = { + hour: 10, + minute: 10, + second: 10, + millisecond: 10, + }; + + expect(service.subtractTime(date, opts).valueOf()).toBeLessThan( + date.valueOf() + ); + }); + }); + + describe('roundDown', () => { + it('should round down date successfully', () => { + const now = new Date(); + const opts: IHelperDateRoundDownOptions = { + hour: true, + millisecond: true, + minute: true, + second: true, + }; + + expect(service.roundDown(now, opts)).toEqual( + moment(now).tz(defTz).startOf('days').toDate() + ); + }); + }); +}); diff --git a/test/common/helper/services/helper.encryption.service.spec.ts b/test/common/helper/services/helper.encryption.service.spec.ts new file mode 100644 index 000000000..800d1875f --- /dev/null +++ b/test/common/helper/services/helper.encryption.service.spec.ts @@ -0,0 +1,212 @@ +import { Test } from '@nestjs/testing'; +import { JwtService } from '@nestjs/jwt'; +import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; +import { AES } from 'crypto-js'; +import { IHelperJwtOptions } from 'src/common/helper/interfaces/helper.interface'; +import { ConfigService } from '@nestjs/config'; + +jest.mock('crypto-js', () => ({ + AES: { + encrypt: jest.fn(), + decrypt: jest.fn(), + }, + enc: { + Utf8: { + parse: jest.fn(), + }, + }, + mode: { + CBC: {}, + }, + pad: { + Pkcs7: {}, + }, +})); + +const mockJwtService = { + sign: jest.fn(), + decode: jest.fn(), + verify: jest.fn(), +}; + +const mockConfigService = { + get: jest.fn().mockImplementation(e => { + switch (e) { + default: + return true; + } + }), +}; + +describe('HelperEncryptionService', () => { + let service: HelperEncryptionService; + let encrypted: string; + let decrypted: string; + let decryptedData: object; + let jwtService: JwtService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + HelperEncryptionService, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + service = moduleRef.get( + HelperEncryptionService + ); + jwtService = moduleRef.get(JwtService); + encrypted = 'ZGF0YQ=='; + decrypted = 'data'; + decryptedData = { + key: 'value', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('base64Encrypt', () => { + it('should encrypt data with base64', () => { + expect(service.base64Encrypt(decrypted)).toEqual(encrypted); + }); + }); + + describe('base64Decrypt', () => { + it('should decrypt data with base64', () => { + expect(service.base64Decrypt(encrypted)).toEqual(decrypted); + }); + }); + + describe('base64Compare', () => { + it('should compare token true', () => { + expect(service.base64Compare(encrypted, encrypted)).toBe(true); + }); + }); + + describe('aes256Encrypt', () => { + it('should encrypt data with AES-256', () => { + const data = { key: 'value' }; + const key = 'encryptionKey'; + const iv = 'initializationVector'; + const result = 'encryptedData'; + + (AES.encrypt as jest.Mock).mockReturnValue(result); + + expect(service.aes256Encrypt(data, key, iv)).toEqual(result); + }); + }); + + describe('aes256Decrypt', () => { + it('should decrypt data with AES-256', () => { + const key = 'encryptionKey'; + const iv = 'initializationVector'; + const result = { + key: 'value', + }; + + (AES.decrypt as jest.Mock).mockReturnValue({ + toString: jest + .fn() + .mockReturnValue(JSON.stringify(decryptedData)), + }); + + expect(service.aes256Decrypt(encrypted, key, iv)).toEqual(result); + }); + }); + + describe('aes256Compare', () => { + it('should compare AES-256', () => { + expect(service.aes256Compare(encrypted, encrypted)).toEqual(true); + }); + }); + + describe('jwtEncrypt', () => { + it('should encrypt data with jwt', () => { + const payload = { + key: 'value', + }; + const opts: IHelperJwtOptions = { + audience: 'audience', + expiredIn: 1, + issuer: 'issuer', + secretKey: 'secretKey', + subject: 'subject', + notBefore: 1, + }; + + (jwtService.sign as jest.Mock).mockReturnValueOnce('jwtSign'); + + expect(service.jwtEncrypt(payload, opts)).toEqual('jwtSign'); + }); + + it('should encrypt data with jwt without notBefore', () => { + const payload = { + key: 'value', + }; + const opts: IHelperJwtOptions = { + audience: 'audience', + expiredIn: 1, + issuer: 'issuer', + secretKey: 'secretKey', + subject: 'subject', + }; + + (jwtService.sign as jest.Mock).mockReturnValueOnce('jwtSign'); + + expect(service.jwtEncrypt(payload, opts)).toEqual('jwtSign'); + }); + }); + + describe('jwtDecrypt', () => { + it('should decrypt token with jwt', () => { + (jwtService.decode as jest.Mock).mockReturnValueOnce('decode'); + expect(service.jwtDecrypt(encrypted)).toEqual('decode'); + }); + }); + + describe('jwtVerify', () => { + it('should verify token with jwt', () => { + const opts: IHelperJwtOptions = { + audience: 'audience', + expiredIn: 1, + issuer: 'issuer', + secretKey: 'secretKey', + subject: 'subject', + notBefore: 1, + }; + expect(service.jwtVerify('token', opts)).toEqual(true); + }); + + it('should return false when token verification fails', () => { + (jwtService.verify as jest.Mock).mockImplementationOnce(() => { + throw new Error('Invalid token'); + }); + + jest.spyOn(service['logger'], 'error').mockImplementation(); + + const opts: IHelperJwtOptions = { + audience: 'audience', + expiredIn: 1, + issuer: 'issuer', + secretKey: 'secretKey', + subject: 'subject', + notBefore: 1, + }; + expect(service.jwtVerify('token', opts)).toEqual(false); + }); + }); +}); diff --git a/test/common/helper/services/helper.hash.service.spec.ts b/test/common/helper/services/helper.hash.service.spec.ts new file mode 100644 index 000000000..74279b1fa --- /dev/null +++ b/test/common/helper/services/helper.hash.service.spec.ts @@ -0,0 +1,72 @@ +import { Test } from '@nestjs/testing'; +import { compareSync, genSaltSync, hashSync } from 'bcryptjs'; +import { SHA256 } from 'crypto-js'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; + +jest.mock('bcryptjs', () => ({ + compareSync: jest.fn(), + genSaltSync: jest.fn(), + hashSync: jest.fn(), +})); +jest.mock('crypto-js', () => ({ + SHA256: jest.fn(), + enc: jest.fn(), +})); + +describe('HelperHashService', () => { + let service: HelperHashService; + let result: string; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [HelperHashService], + }).compile(); + + service = moduleRef.get(HelperHashService); + result = 'result'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('randomSalt', () => { + it('should return random salt', () => { + (genSaltSync as jest.Mock).mockReturnValue(result); + expect(service.randomSalt(10)).toEqual(result); + }); + }); + + describe('bcrypt', () => { + it('should return hash', () => { + (hashSync as jest.Mock).mockReturnValue(result); + expect(service.bcrypt('password', 'salt')).toEqual(result); + }); + }); + + describe('bcryptCompare', () => { + it('should compare password', () => { + (compareSync as jest.Mock).mockReturnValue(true); + expect(service.bcryptCompare('password', 'passwordHashed')).toEqual( + true + ); + }); + }); + + describe('sha256', () => { + it('should convert to sha256', () => { + (SHA256 as jest.Mock).mockReturnValue(result); + expect(service.sha256('password')).toEqual(result); + }); + }); + + describe('sha256Compare', () => { + it('should compare sha256', () => { + expect(service.sha256Compare('password', 'password')).toEqual(true); + }); + }); +}); diff --git a/test/common/helper/services/helper.number.service.spec.ts b/test/common/helper/services/helper.number.service.spec.ts new file mode 100644 index 000000000..ed4faa061 --- /dev/null +++ b/test/common/helper/services/helper.number.service.spec.ts @@ -0,0 +1,52 @@ +import { Test } from '@nestjs/testing'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; + +describe('HelperNumberService', () => { + let service: HelperNumberService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [HelperNumberService], + }).compile(); + + service = moduleRef.get(HelperNumberService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('check', () => { + it('should check number', () => { + expect(service.check('10')).toEqual(true); + }); + }); + + describe('random', () => { + it('should random number', () => { + jest.spyOn(service, 'randomInRange').mockReturnValueOnce(1234); + + expect(service.random(4)).toEqual(1234); + }); + }); + + describe('randomInRange', () => { + it('should random in range number', () => { + expect(service.randomInRange(1, 1)).toEqual(1); + }); + }); + + describe('percent', () => { + it('should return percentage', () => { + expect(service.percent(1, 10)).toEqual(10); + }); + + it('should return percentage with unknown value', () => { + expect(service.percent(NaN, 10)).toEqual(0); + }); + }); +}); diff --git a/test/common/helper/services/helper.string.service.spec.ts b/test/common/helper/services/helper.string.service.spec.ts new file mode 100644 index 000000000..710fee725 --- /dev/null +++ b/test/common/helper/services/helper.string.service.spec.ts @@ -0,0 +1,112 @@ +import { Test } from '@nestjs/testing'; +import { + IHelperStringCurrencyOptions, + IHelperStringPasswordOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +describe('HelperStringService', () => { + let service: HelperStringService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [HelperStringService], + }).compile(); + + service = moduleRef.get(HelperStringService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('randomReference', () => { + it('should return random string', () => { + const now = new Date(); + const random = 'RANDOM'; + + jest.spyOn(global, 'Date').mockImplementation(() => now); + jest.spyOn(service, 'random').mockReturnValue(random); + expect(service.randomReference(3)).toEqual( + `${now.getTime()}${random}` + ); + }); + }); + + describe('random', () => { + it('should return random alphanumeric', () => { + expect(service.random(10)).toMatch(/^[a-zA-Z0-9]+$/); + }); + }); + + describe('censor', () => { + it('should sensor text with length > 10', () => { + const text = 'abcdefghijkl'; + const result = 'abc**********ijkl'; + + expect(service.censor(text)).toEqual(result); + }); + + it('should sensor text with length <= 10', () => { + const text = 'abcdefghij'; + const result = '*******hij'; + + expect(service.censor(text)).toEqual(result); + }); + + it('should sensor text with length <= 3', () => { + const text = 'abc'; + const result = '**c'; + + expect(service.censor(text)).toEqual(result); + }); + }); + + describe('checkEmail', () => { + it('should return true', () => { + expect(service.checkEmail('akan@kadence.com')).toBe(true); + }); + }); + + describe('checkPasswordStrength', () => { + it('should return true', () => { + expect(service.checkPasswordStrength('P4ssword')).toBe(true); + }); + + it('should return true if length == 4', () => { + const opts: IHelperStringPasswordOptions = { + length: 4, + }; + expect(service.checkPasswordStrength('P4ss', opts)).toBe(true); + }); + + it('should return false', () => { + expect(service.checkPasswordStrength('password')).toBe(false); + }); + }); + + describe('checkSafeString', () => { + it('should return true', () => { + expect(service.checkSafeString('safestring')).toBe(true); + }); + + it('should return false', () => { + expect(service.checkSafeString('!@#')).toBe(false); + }); + }); + + describe('formatCurrency', () => { + it('should return formatted currency', () => { + const number = 1000000; + const opts: IHelperStringCurrencyOptions = { + locale: 'id-ID', + }; + + expect(service.formatCurrency(number, opts)).toEqual('1.000.000'); + }); + }); +}); diff --git a/test/common/message/services/message.service.spec.ts b/test/common/message/services/message.service.spec.ts new file mode 100644 index 000000000..a30625b44 --- /dev/null +++ b/test/common/message/services/message.service.spec.ts @@ -0,0 +1,402 @@ +import { Test } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { I18nService } from 'nestjs-i18n'; +import { ValidationError } from 'class-validator'; +import { MessageService } from 'src/common/message/services/message.service'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { IMessageValidationImportErrorParam } from 'src/common/message/interfaces/message.interface'; + +describe('MessageService', () => { + let service: MessageService; + + const language = 'en'; + const language2 = 'id'; + const mockMessage = 'Localized Message'; + + let validationError: ValidationError[]; + let validationError2: ValidationError[]; + let validationError3: ValidationError[]; + let validationError4: ValidationError[]; + let validationErrorImport: IMessageValidationImportErrorParam[]; + + beforeEach(async () => { + const moduleRefRef = await Test.createTestingModule({ + providers: [ + MessageService, + HelperArrayService, + { + provide: I18nService, + useValue: { + translate: jest.fn().mockReturnValue(mockMessage), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'message.language': + return language; + case 'message.availableLanguage': + default: + return [language, language2]; + } + }), + }, + }, + ], + }).compile(); + + service = moduleRefRef.get(MessageService); + + validationError = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + children: [], + constraints: { isEmail: 'email must be an email' }, + }, + ]; + + validationError2 = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { isEmail: 'email must be an email' }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [], + }, + ], + }, + ]; + + validationError3 = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { isEmail: 'email must be an email' }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [], + }, + ], + }, + ], + }, + ]; + + validationError4 = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + }, + ]; + + validationErrorImport = [ + { + row: 0, + errors: [ + { + target: { + number: 1, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8272 }, + address: 'address 1', + tags: ['test', 'lala'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 1, + errors: [ + { + target: { + number: 2, + area: 'area', + city: 'area timur', + tags: [], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 2, + errors: [ + { + target: { + number: null, + area: 'area', + city: 'area timur', + address: 'address 3', + tags: ['test'], + }, + value: null, + property: 'number', + children: [], + constraints: { + min: 'number must not be less than 0', + isNumber: + 'number must be a number conforming to the specified constraints', + isString: 'mainBranch must be a string', + }, + }, + { + target: { + number: null, + area: 'area', + city: 'area timur', + address: 'address 3', + tags: ['test'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + }, + }, + ], + }, + { + row: 3, + errors: [ + { + target: { + number: 4, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8273 }, + address: 'address 4', + tags: ['hand', 'test'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + }, + }, + ], + }, + { + row: 4, + errors: [ + { + target: { + number: null, + area: 'area', + city: 'area timur', + tags: ['lala'], + }, + value: null, + property: 'number', + children: [], + constraints: { + min: 'number must not be less than 0', + isNumber: + 'number must be a number conforming to the specified constraints', + }, + }, + { + target: { + number: null, + area: 'area', + city: 'area timur', + tags: ['lala'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 5, + errors: [ + { + target: { + number: 6, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8273 }, + address: 'address 6', + tags: [], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + ]; + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getAvailableLanguages', () => { + it('should return an array of available languages', () => { + expect(service.getAvailableLanguages()).toEqual([ + language, + language2, + ]); + }); + }); + + describe('getLanguage', () => { + it('should return the default language', () => { + expect(service.getLanguage()).toEqual(language); + }); + }); + + describe('filterLanguage', () => { + it('should return the same array of languages available', () => { + const languages = service.filterLanguage(language); + + expect(languages.length).toEqual(1); + expect(languages).toEqual([language]); + }); + }); + + describe('setMessage', () => { + it('should return the translated message for the given language and key', () => { + expect(service.setMessage('test.message')).toEqual(mockMessage); + }); + + it('should include the properties in the translated message', () => { + expect( + service.setMessage('test.message', { + customLanguage: language, + properties: { name: 'John' }, + }) + ).toEqual(mockMessage); + }); + }); + + describe('setValidationMessage', () => { + it('should return an array of error messages corresponding to the validation errors', () => { + const message = service.setValidationMessage(validationError); + + expect(message).toEqual([ + { message: 'Localized Message', property: 'email' }, + ]); + }); + + it('should return an array of error messages corresponding to the validation errors for nested object', () => { + const message = service.setValidationMessage(validationError2); + + expect(message).toEqual([ + { + message: 'Localized Message', + property: 'email', + }, + ]); + }); + + it('should return an array of error messages corresponding to the validation errors for deepest nested object', () => { + const message = service.setValidationMessage(validationError3); + + expect(message).toEqual([ + { + message: 'Localized Message', + property: 'email', + }, + ]); + }); + + it('should return an array of error messages corresponding to the validation without constraints field', () => { + const message = service.setValidationMessage(validationError4); + + expect(message).toEqual([ + { + message: 'Localized Message', + property: 'email', + }, + ]); + }); + }); + + describe('setValidationImportMessage', () => { + it('should return an array of error messages corresponding to the validation errors in the import', () => { + const message = service.setValidationImportMessage( + validationErrorImport + ); + + expect(message.length).toEqual(validationErrorImport.length); + expect(message[0].errors.length).toEqual(2); + expect(message[0].row).toBeDefined(); + }); + }); +}); diff --git a/test/common/pagination/decorators/pagination.decorator.spec.ts b/test/common/pagination/decorators/pagination.decorator.spec.ts new file mode 100644 index 000000000..25a3b22e7 --- /dev/null +++ b/test/common/pagination/decorators/pagination.decorator.spec.ts @@ -0,0 +1,312 @@ +import { + PaginationQuery, + PaginationQueryFilterDate, + PaginationQueryFilterEqual, + PaginationQueryFilterInBoolean, + PaginationQueryFilterInEnum, + PaginationQueryFilterNinEnum, + PaginationQueryFilterNotEqual, + PaginationQueryFilterStringContain, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { PaginationFilterDatePipe } from 'src/common/pagination/pipes/pagination.filter-date.pipe'; +import { PaginationFilterEqualPipe } from 'src/common/pagination/pipes/pagination.filter-equal.pipe'; +import { PaginationFilterInBooleanPipe } from 'src/common/pagination/pipes/pagination.filter-in-boolean.pipe'; +import { PaginationFilterInEnumPipe } from 'src/common/pagination/pipes/pagination.filter-in-enum.pipe'; +import { PaginationFilterNinEnumPipe } from 'src/common/pagination/pipes/pagination.filter-nin-enum.pipe'; +import { PaginationFilterNotEqualPipe } from 'src/common/pagination/pipes/pagination.filter-not-equal.pipe'; +import { PaginationFilterStringContainPipe } from 'src/common/pagination/pipes/pagination.filter-string-contain.pipe'; +import { PaginationOrderPipe } from 'src/common/pagination/pipes/pagination.order.pipe'; +import { PaginationPagingPipe } from 'src/common/pagination/pipes/pagination.paging.pipe'; +import { PaginationSearchPipe } from 'src/common/pagination/pipes/pagination.search.pipe'; + +jest.mock('src/common/pagination/pipes/pagination.filter-date.pipe', () => ({ + PaginationFilterDatePipe: jest.fn(), +})); +jest.mock('src/common/pagination/pipes/pagination.filter-equal.pipe', () => ({ + PaginationFilterEqualPipe: jest.fn(), +})); +jest.mock( + 'src/common/pagination/pipes/pagination.filter-in-boolean.pipe', + () => ({ + PaginationFilterInBooleanPipe: jest.fn(), + }) +); +jest.mock('src/common/pagination/pipes/pagination.filter-in-enum.pipe', () => ({ + PaginationFilterInEnumPipe: jest.fn(), +})); +jest.mock( + 'src/common/pagination/pipes/pagination.filter-nin-enum.pipe', + () => ({ + PaginationFilterNinEnumPipe: jest.fn(), + }) +); +jest.mock( + 'src/common/pagination/pipes/pagination.filter-not-equal.pipe', + () => ({ + PaginationFilterNotEqualPipe: jest.fn(), + }) +); +jest.mock( + 'src/common/pagination/pipes/pagination.filter-string-contain.pipe', + () => ({ + PaginationFilterStringContainPipe: jest.fn(), + }) +); +jest.mock('src/common/pagination/pipes/pagination.order.pipe', () => ({ + PaginationOrderPipe: jest.fn(), +})); +jest.mock('src/common/pagination/pipes/pagination.paging.pipe', () => ({ + PaginationPagingPipe: jest.fn(), +})); +jest.mock('src/common/pagination/pipes/pagination.search.pipe', () => ({ + PaginationSearchPipe: jest.fn(), +})); + +describe('Pagination Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('PaginationQuery', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQuery(); + + expect(result).toBeTruthy(); + expect(PaginationSearchPipe).toHaveBeenCalledWith(undefined); + expect(PaginationPagingPipe).toHaveBeenCalledWith(undefined); + expect(PaginationOrderPipe).toHaveBeenCalledWith( + undefined, + undefined, + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQuery({ + availableOrderBy: ['createdAt'], + availableSearch: ['name'], + defaultOrderBy: 'createdAt', + defaultOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + defaultPerPage: 10, + }); + + expect(result).toBeTruthy(); + expect(PaginationSearchPipe).toHaveBeenCalledWith(['name']); + expect(PaginationPagingPipe).toHaveBeenCalledWith(10); + expect(PaginationOrderPipe).toHaveBeenCalledWith( + 'createdAt', + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ['createdAt'] + ); + }); + }); + + describe('PaginationQueryFilterInBoolean', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterInBoolean('test', [ + true, + false, + ]); + + expect(result).toBeTruthy(); + expect(PaginationFilterInBooleanPipe).toHaveBeenCalledWith( + 'test', + [true, false], + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterInBoolean( + 'test', + [true, false], + { + queryField: 'testQuery', + } + ); + + expect(result).toBeTruthy(); + expect(PaginationFilterInBooleanPipe).toHaveBeenCalledWith( + 'test', + [true, false], + { + queryField: 'testQuery', + } + ); + }); + }); + + describe('PaginationQueryFilterInEnum', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterInEnum( + 'test', + ['12345'], + ['12345', '67890'] + ); + + expect(result).toBeTruthy(); + expect(PaginationFilterInEnumPipe).toHaveBeenCalledWith( + 'test', + ['12345'], + ['12345', '67890'], + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterInEnum( + 'test', + ['12345'], + ['12345', '67890'], + { + queryField: 'testQuery', + } + ); + + expect(result).toBeTruthy(); + expect(PaginationFilterInEnumPipe).toHaveBeenCalledWith( + 'test', + ['12345'], + ['12345', '67890'], + { + queryField: 'testQuery', + } + ); + }); + }); + + describe('PaginationQueryFilterNinEnum', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterNinEnum( + 'test', + ['12345'], + ['12345', '67890'] + ); + + expect(result).toBeTruthy(); + expect(PaginationFilterNinEnumPipe).toHaveBeenCalledWith( + 'test', + ['12345'], + ['12345', '67890'], + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterNinEnum( + 'test', + ['12345'], + ['12345', '67890'], + { + queryField: 'testQuery', + } + ); + + expect(result).toBeTruthy(); + expect(PaginationFilterNinEnumPipe).toHaveBeenCalledWith( + 'test', + ['12345'], + ['12345', '67890'], + { + queryField: 'testQuery', + } + ); + }); + }); + + describe('PaginationQueryFilterNotEqual', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterNotEqual('test'); + + expect(result).toBeTruthy(); + expect(PaginationFilterNotEqualPipe).toHaveBeenCalledWith( + 'test', + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterNotEqual('test', { + queryField: 'testQuery', + }); + + expect(result).toBeTruthy(); + expect(PaginationFilterNotEqualPipe).toHaveBeenCalledWith('test', { + queryField: 'testQuery', + }); + }); + }); + + describe('PaginationQueryFilterEqual', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterEqual('test'); + + expect(result).toBeTruthy(); + expect(PaginationFilterEqualPipe).toHaveBeenCalledWith( + 'test', + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterEqual('test', { + queryField: 'testQuery', + }); + + expect(result).toBeTruthy(); + expect(PaginationFilterEqualPipe).toHaveBeenCalledWith('test', { + queryField: 'testQuery', + }); + }); + }); + + describe('PaginationQueryFilterStringContain', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterStringContain('test'); + + expect(result).toBeTruthy(); + expect(PaginationFilterStringContainPipe).toHaveBeenCalledWith( + 'test', + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterStringContain('test', { + queryField: 'testQuery', + }); + + expect(result).toBeTruthy(); + expect(PaginationFilterStringContainPipe).toHaveBeenCalledWith( + 'test', + { + queryField: 'testQuery', + } + ); + }); + }); + + describe('PaginationQueryFilterDate', () => { + it('Should return applyDecorators', async () => { + const result = PaginationQueryFilterDate('test'); + + expect(result).toBeTruthy(); + expect(PaginationFilterDatePipe).toHaveBeenCalledWith( + 'test', + undefined + ); + }); + + it('Should return applyDecorators with options', async () => { + const result = PaginationQueryFilterDate('test', { + queryField: 'testQuery', + }); + + expect(result).toBeTruthy(); + expect(PaginationFilterDatePipe).toHaveBeenCalledWith('test', { + queryField: 'testQuery', + }); + }); + }); +}); diff --git a/test/common/pagination/dtos/pagination.list.dto.spec.ts b/test/common/pagination/dtos/pagination.list.dto.spec.ts new file mode 100644 index 000000000..a7910e1ab --- /dev/null +++ b/test/common/pagination/dtos/pagination.list.dto.spec.ts @@ -0,0 +1,42 @@ +import { plainToInstance } from 'class-transformer'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; + +describe('PaginationListDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: PaginationListDto = { + _search: { name: 'testSearch' }, + _limit: 10, + _offset: 1, + _order: { + createdAt: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }, + _availableOrderBy: ['createAt'], + _availableOrderDirection: Object.values( + ENUM_PAGINATION_ORDER_DIRECTION_TYPE + ), + perPage: 10, + page: 1, + orderBy: 'createdAt', + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }; + + const dto = plainToInstance(PaginationListDto, response); + + expect(dto).toBeInstanceOf(PaginationListDto); + expect(dto._search).toBeDefined(); + expect(dto._limit).toBeDefined(); + expect(dto._offset).toBeDefined(); + expect(dto._order).toBeDefined(); + expect(dto._availableOrderBy).toBeDefined(); + expect(dto._availableOrderDirection).toBeDefined(); + expect(dto.perPage).toBeDefined(); + expect(dto.page).toBeDefined(); + expect(dto.orderBy).toBeDefined(); + expect(dto.orderDirection).toBeDefined(); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-date.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-date.pipe.spec.ts new file mode 100644 index 000000000..c18b529f7 --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-date.pipe.spec.ts @@ -0,0 +1,174 @@ +import { PipeTransform } from '@nestjs/common'; +import moment from 'moment'; +import { DatabaseQueryEqual } from 'src/common/database/decorators/database.decorator'; +import { ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS } from 'src/common/pagination/enums/pagination.enum'; +import { PaginationFilterDatePipe } from 'src/common/pagination/pipes/pagination.filter-date.pipe'; + +describe('PaginationFilterDatePipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionEndDay: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionStartDay: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + filterDate: jest + .fn() + .mockImplementation((a: string, b: Date) => + DatabaseQueryEqual(a, b) + ), + }; + + const mockHelperDateService = { + create: jest.fn(), + endOfDay: jest.fn(), + startOfDay: jest.fn(), + }; + + beforeEach(() => { + const mixin = PaginationFilterDatePipe('test'); + pipe = new mixin( + mockRequest, + mockPaginationService, + mockHelperDateService + ) as any; + + const mixinOption = PaginationFilterDatePipe('test', { + raw: true, + }); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService, + mockHelperDateService + ) as any; + + const mixinEndDay = PaginationFilterDatePipe('test', { + time: ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.END_OF_DAY, + }); + pipeOptionEndDay = new mixinEndDay( + mockRequest, + mockPaginationService, + mockHelperDateService + ) as any; + + const mixinOStartDay = PaginationFilterDatePipe('test', { + time: ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.START_OF_DAY, + }); + pipeOptionStartDay = new mixinOStartDay( + mockRequest, + mockPaginationService, + mockHelperDateService + ) as any; + + const mixin2 = PaginationFilterDatePipe('test'); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService, + mockHelperDateService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + expect(pipeOptionEndDay).toBeDefined(); + expect(pipeOptionStartDay).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should return undefined if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeUndefined(); + }); + + it('Should return raw if raw options is true', async () => { + const today = new Date(); + const result = await pipeOptionRaw.transform(today.toISOString()); + + expect(result).toBeDefined(); + expect(result).toEqual({ + test: today.toISOString(), + }); + }); + + it('Should convert date to end of day if option time is END DAY', async () => { + const today = new Date(); + const endDay = moment(today).endOf('day').toDate(); + mockHelperDateService.endOfDay.mockReturnValue(endDay); + + const result = await pipeOptionEndDay.transform( + today.toISOString() + ); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryEqual('test', endDay)); + }); + + it('Should convert date to end of day if option time is START DAY', async () => { + const today = new Date(); + const startDay = moment(today).startOf('day').toDate(); + mockHelperDateService.startOfDay.mockReturnValue(startDay); + + const result = await pipeOptionStartDay.transform( + today.toISOString() + ); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryEqual('test', startDay)); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('string'); + + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-equal.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-equal.pipe.spec.ts new file mode 100644 index 000000000..33cedf28c --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-equal.pipe.spec.ts @@ -0,0 +1,124 @@ +import { PipeTransform } from '@nestjs/common'; +import { DatabaseQueryEqual } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterEqualPipe } from 'src/common/pagination/pipes/pagination.filter-equal.pipe'; + +describe('PaginationFilterEqualPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionIsNumber: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + filterEqual: jest + .fn() + .mockImplementation((a: string, b: string) => + DatabaseQueryEqual(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterEqualPipe('test'); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinOption = PaginationFilterEqualPipe('test', { + raw: true, + }); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService + ) as any; + + const mixinIsNumber = PaginationFilterEqualPipe('test', { + isNumber: true, + }); + pipeOptionIsNumber = new mixinIsNumber( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationFilterEqualPipe('test'); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + expect(pipeOptionIsNumber).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should return undefined if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeUndefined(); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('string'); + + expect(result).toBeDefined(); + expect(result).toEqual({ + test: 'string', + }); + }); + + it('Should convert string to number if option isNumber is true', async () => { + const result = await pipeOptionIsNumber.transform('1'); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryEqual('test', 1)); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('string'); + + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-in-boolean.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-in-boolean.pipe.spec.ts new file mode 100644 index 000000000..63b411ed0 --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-in-boolean.pipe.spec.ts @@ -0,0 +1,118 @@ +import { PipeTransform } from '@nestjs/common'; +import _ from 'lodash'; +import { DatabaseQueryIn } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterInBooleanPipe } from 'src/common/pagination/pipes/pagination.filter-in-boolean.pipe'; + +describe('PaginationFilterInBooleanPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockHelperArrayService = { + unique: jest.fn().mockImplementation(e => _.uniq(e)), + }; + + const mockPaginationService = { + filterIn: jest + .fn() + .mockImplementation((a: string, b: boolean[]) => + DatabaseQueryIn(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterInBooleanPipe('test', [true, false]); + pipe = new mixin( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixinOption = PaginationFilterInBooleanPipe( + 'test', + [true, false], + { + raw: true, + } + ); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixin2 = PaginationFilterInBooleanPipe('test', [true, false]); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService, + mockHelperArrayService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOption2).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + }); + + describe('transform', () => { + it('Should return default value if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryIn('test', [true, false])); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('true,false'); + + expect(result).toBeDefined(); + expect(result).toEqual({ test: 'true,false' }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('true,false'); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryIn('test', [true, false])); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-in-enum.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-in-enum.pipe.spec.ts new file mode 100644 index 000000000..f6e2e8d48 --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-in-enum.pipe.spec.ts @@ -0,0 +1,129 @@ +import { PipeTransform } from '@nestjs/common'; +import _ from 'lodash'; +import { DatabaseQueryIn } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterInEnumPipe } from 'src/common/pagination/pipes/pagination.filter-in-enum.pipe'; + +describe('PaginationFilterInEnumPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockHelperArrayService = { + getIntersection: jest + .fn() + .mockImplementation((a, b) => _.intersection(a, b)), + }; + + const mockPaginationService = { + filterIn: jest + .fn() + .mockImplementation((a: string, b: string[]) => + DatabaseQueryIn(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterInEnumPipe( + 'test', + ['123'], + ['123', '321'] + ); + pipe = new mixin( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixinOption = PaginationFilterInEnumPipe( + 'test', + ['123'], + ['123', '321'], + { + raw: true, + } + ); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixin2 = PaginationFilterInEnumPipe( + 'test', + ['123'], + ['123', '321'] + ); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService, + mockHelperArrayService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOption2).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + }); + + describe('transform', () => { + it('Should return default value if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryIn('test', ['123'])); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('asd,qwerty'); + + expect(result).toBeDefined(); + expect(result).toEqual({ test: 'asd,qwerty' }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('123,qwerty'); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryIn('test', ['123'])); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-nin-enum.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-nin-enum.pipe.spec.ts new file mode 100644 index 000000000..16be5b74c --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-nin-enum.pipe.spec.ts @@ -0,0 +1,129 @@ +import { PipeTransform } from '@nestjs/common'; +import _ from 'lodash'; +import { DatabaseQueryNin } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterNinEnumPipe } from 'src/common/pagination/pipes/pagination.filter-nin-enum.pipe'; + +describe('PaginationFilterNinEnumPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockHelperArrayService = { + getIntersection: jest + .fn() + .mockImplementation((a, b) => _.intersection(a, b)), + }; + + const mockPaginationService = { + filterNin: jest + .fn() + .mockImplementation((a: string, b: string[]) => + DatabaseQueryNin(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterNinEnumPipe( + 'test', + ['123'], + ['123', '321'] + ); + pipe = new mixin( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixinOption = PaginationFilterNinEnumPipe( + 'test', + ['123'], + ['123', '321'], + { + raw: true, + } + ); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService, + mockHelperArrayService + ) as any; + + const mixin2 = PaginationFilterNinEnumPipe( + 'test', + ['123'], + ['123', '321'] + ); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService, + mockHelperArrayService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOption2).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + }); + + describe('transform', () => { + it('Should return default value if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryNin('test', ['123'])); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('asd,qwerty'); + + expect(result).toBeDefined(); + expect(result).toEqual({ test: 'asd,qwerty' }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('123,qwerty'); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryNin('test', ['123'])); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-not-equal.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-not-equal.pipe.spec.ts new file mode 100644 index 000000000..cbfda9bbc --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-not-equal.pipe.spec.ts @@ -0,0 +1,122 @@ +import { PipeTransform } from '@nestjs/common'; +import { DatabaseQueryNotEqual } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterNotEqualPipe } from 'src/common/pagination/pipes/pagination.filter-not-equal.pipe'; + +describe('PaginationFilterNotEqualPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionIsNumber: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + filterNotEqual: jest + .fn() + .mockImplementation((a: string, b: string) => + DatabaseQueryNotEqual(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterNotEqualPipe('test'); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinOption = PaginationFilterNotEqualPipe('test', { + raw: true, + }); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService + ) as any; + + const mixinIsNumber = PaginationFilterNotEqualPipe('test', { + isNumber: true, + }); + pipeOptionIsNumber = new mixinIsNumber( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationFilterNotEqualPipe('test'); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + }); + + describe('transform', () => { + it('Should return undefined if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeUndefined(); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('string'); + + expect(result).toBeDefined(); + expect(result).toEqual({ + test: 'string', + }); + }); + + it('Should convert string to number if option isNumber is true', async () => { + const result = await pipeOptionIsNumber.transform('1'); + + expect(result).toBeDefined(); + expect(result).toEqual(DatabaseQueryNotEqual('test', 1)); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('string'); + + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.filter-string-contain.pipe.spec.ts b/test/common/pagination/pipes/pagination.filter-string-contain.pipe.spec.ts new file mode 100644 index 000000000..e6732303a --- /dev/null +++ b/test/common/pagination/pipes/pagination.filter-string-contain.pipe.spec.ts @@ -0,0 +1,103 @@ +import { PipeTransform } from '@nestjs/common'; +import { DatabaseQueryContain } from 'src/common/database/decorators/database.decorator'; +import { PaginationFilterStringContainPipe } from 'src/common/pagination/pipes/pagination.filter-string-contain.pipe'; + +describe('PaginationFilterStringContainPipe', () => { + let pipe: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOptionRaw: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: string) => Promise>; + addToRequestInstance: (value: any) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + filterContain: jest + .fn() + .mockImplementation((a: string, b: string) => + DatabaseQueryContain(a, b) + ), + }; + + beforeEach(() => { + const mixin = PaginationFilterStringContainPipe('test'); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinOption = PaginationFilterStringContainPipe('test', { + raw: true, + }); + pipeOptionRaw = new mixinOption( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationFilterStringContainPipe('test'); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeOptionRaw).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should return undefined if value is undefined', async () => { + const result = await pipe.transform(undefined); + + expect(result).toBeUndefined(); + }); + + it('Should return raw if raw options is true', async () => { + const result = await pipeOptionRaw.transform('string'); + + expect(result).toBeDefined(); + expect(result).toEqual({ + test: 'string', + }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('string'); + + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance('string'); + + expect(mockRequestWithoutFilter.__pagination.filters).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.filters.test).toEqual( + 'string' + ); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.order.pipe.spec.ts b/test/common/pagination/pipes/pagination.order.pipe.spec.ts new file mode 100644 index 000000000..df843e19c --- /dev/null +++ b/test/common/pagination/pipes/pagination.order.pipe.spec.ts @@ -0,0 +1,164 @@ +import { PipeTransform } from '@nestjs/common'; +import { PAGINATION_DEFAULT_ORDER_DIRECTION } from 'src/common/pagination/constants/pagination.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { PaginationOrderPipe } from 'src/common/pagination/pipes/pagination.order.pipe'; + +describe('PaginationOrderPipe', () => { + let pipe: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + orderBy: string, + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[], + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] + ) => void; + }; + + let pipeDefault: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + orderBy: string, + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[], + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] + ) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + orderBy: string, + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[], + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] + ) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + order: jest.fn().mockReturnValue({ + createdAt: PAGINATION_DEFAULT_ORDER_DIRECTION, + }), + }; + + beforeEach(() => { + const mixin = PaginationOrderPipe( + 'createdAt', + PAGINATION_DEFAULT_ORDER_DIRECTION, + ['createdAt'] + ); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinDefault = PaginationOrderPipe(); + pipeDefault = new mixinDefault( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationOrderPipe( + 'createdAt', + PAGINATION_DEFAULT_ORDER_DIRECTION, + ['createdAt'] + ); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeDefault).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should be successful calls with default', async () => { + const result = await pipe.transform({}); + + expect(mockPaginationService.order).toHaveBeenCalledWith( + 'createdAt', + 'asc', + ['createdAt'] + ); + expect(result).toBeDefined(); + expect(result).toEqual({ + _availableOrderBy: ['createdAt'], + _availableOrderDirection: ['asc', 'desc'], + _order: { + createdAt: 'asc', + }, + }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform({ + orderBy: 'createdAt', + orderDirection: 'asc', + }); + + expect(mockPaginationService.order).toHaveBeenCalledWith( + 'createdAt', + 'asc', + ['createdAt'] + ); + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance( + 'createdAt', + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ['createdAt'], + [ + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.DESC, + ] + ); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.orderBy).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.orderDirection + ).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableOrderBy + ).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableOrderDirection + ).toBeDefined(); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance( + 'createdAt', + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ['createdAt'], + [ + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.DESC, + ] + ); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.orderBy).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.orderDirection + ).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableOrderBy + ).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableOrderDirection + ).toBeDefined(); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.paging.pipe.spec.ts b/test/common/pagination/pipes/pagination.paging.pipe.spec.ts new file mode 100644 index 000000000..f5694b03e --- /dev/null +++ b/test/common/pagination/pipes/pagination.paging.pipe.spec.ts @@ -0,0 +1,100 @@ +import { PipeTransform } from '@nestjs/common'; +import { PaginationPagingPipe } from 'src/common/pagination/pipes/pagination.paging.pipe'; + +describe('PaginationPagingPipe', () => { + let pipe: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: (page: number, perPage: number) => void; + }; + + let pipeDefault: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: (page: number, perPage: number) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: (page: number, perPage: number) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + page: jest.fn().mockReturnValue(1), + offset: jest.fn().mockReturnValue(0), + perPage: jest.fn().mockReturnValue(10), + }; + + beforeEach(() => { + const mixin = PaginationPagingPipe(10); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinDefault = PaginationPagingPipe(); + pipeDefault = new mixinDefault( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationPagingPipe(10); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeDefault).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should be successful calls with default', async () => { + const result = await pipe.transform({}); + + expect(mockPaginationService.perPage).toHaveBeenCalledWith(10); + expect(mockPaginationService.offset).toHaveBeenCalledWith(1, 10); + expect(result).toBeDefined(); + expect(result).toEqual({ + _limit: 10, + _offset: 0, + page: 1, + perPage: 10, + }); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform({ + perPage: '10', + page: '1', + }); + + expect(mockPaginationService.perPage).toHaveBeenCalledWith(10); + expect(mockPaginationService.offset).toHaveBeenCalledWith(1, 10); + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance(1, 10); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.perPage).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.page).toBeDefined(); + }); + + it('Should be successful calls', async () => { + pipe.addToRequestInstance(1, 10); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.perPage).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.page).toBeDefined(); + }); + }); +}); diff --git a/test/common/pagination/pipes/pagination.search.pipe.spec.ts b/test/common/pagination/pipes/pagination.search.pipe.spec.ts new file mode 100644 index 000000000..1791c4b1b --- /dev/null +++ b/test/common/pagination/pipes/pagination.search.pipe.spec.ts @@ -0,0 +1,104 @@ +import { PipeTransform } from '@nestjs/common'; +import { PaginationSearchPipe } from 'src/common/pagination/pipes/pagination.search.pipe'; + +describe('PaginationSearchPipe', () => { + let pipe: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + search: string, + availableSearch: string[] + ) => void; + }; + + let pipeDefault: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + search: string, + availableSearch: string[] + ) => void; + }; + + let pipeOption2: PipeTransform & { + transform: (value: Record) => Promise>; + addToRequestInstance: ( + search: string, + availableSearch: string[] + ) => void; + }; + + const mockRequest = { __pagination: { filters: {} } }; + const mockRequestWithoutFilter = { __pagination: {} as any }; + + const mockPaginationService = { + search: jest.fn(), + }; + + beforeEach(() => { + const mixin = PaginationSearchPipe(['name']); + pipe = new mixin(mockRequest, mockPaginationService) as any; + + const mixinDefault = PaginationSearchPipe(); + pipeDefault = new mixinDefault( + mockRequest, + mockPaginationService + ) as any; + + const mixin2 = PaginationSearchPipe(['name']); + pipeOption2 = new mixin2( + mockRequestWithoutFilter, + mockPaginationService + ) as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + expect(pipeDefault).toBeDefined(); + expect(pipeOption2).toBeDefined(); + }); + + describe('transform', () => { + it('Should be successful calls and return undefined if search text is undefined', async () => { + const result = await pipe.transform({}); + + expect(result).toBeDefined(); + expect(result).toEqual({}); + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform({ + search: 'test', + }); + + expect(mockPaginationService.search).toHaveBeenCalledWith('test', [ + 'name', + ]); + expect(result).toBeDefined(); + }); + }); + + describe('addToRequestInstance', () => { + it('Should be successful calls without pagination filters', async () => { + pipeOption2.addToRequestInstance('test', ['name']); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.search).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableSearch + ).toBeDefined(); + }); + + it('Should be successful calls', async () => { + pipeOption2.addToRequestInstance('test', ['name']); + + expect(mockRequestWithoutFilter.__pagination).toBeDefined(); + expect(mockRequestWithoutFilter.__pagination.search).toBeDefined(); + expect( + mockRequestWithoutFilter.__pagination.availableSearch + ).toBeDefined(); + }); + }); +}); diff --git a/test/common/pagination/services/pagination.service.spec.ts b/test/common/pagination/services/pagination.service.spec.ts new file mode 100644 index 000000000..2de8f42d9 --- /dev/null +++ b/test/common/pagination/services/pagination.service.spec.ts @@ -0,0 +1,199 @@ +import { Test } from '@nestjs/testing'; +import { + DatabaseQueryContain, + DatabaseQueryEqual, + DatabaseQueryIn, + DatabaseQueryNin, + DatabaseQueryNotEqual, + DatabaseQueryOr, +} from 'src/common/database/decorators/database.decorator'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; + +describe('PaginationService', () => { + let service: PaginationService; + + beforeEach(async () => { + const moduleRefRef = await Test.createTestingModule({ + providers: [PaginationService], + }).compile(); + + service = moduleRefRef.get(PaginationService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('offset', () => { + it('should offset the given page and perPage', () => { + expect(service.offset(1, 10)).toBe(0); + }); + + it('should limit the page to the maximum allowed value', () => { + expect(service.offset(1000, 10)).toBe(190); + }); + + it('should limit the perPage to the maximum allowed value', () => { + expect(service.offset(1, 1000)).toBe(0); + }); + }); + + describe('totalPage', () => { + it('should calculate the total number of pages', () => { + expect(service.totalPage(100, 10)).toBe(10); + }); + + it('should return 1 if totalData is 0', () => { + expect(service.totalPage(0, 10)).toBe(1); + }); + + it('should limit the total number of pages to the maximum allowed value', () => { + expect(service.totalPage(1000, 10)).toBe(20); + }); + }); + + describe('page', () => { + it('should return the default page value if no page parameter is provided', () => { + expect(service.page()).toBe(1); + }); + + it('should return the page 3 value', () => { + expect(service.page(3)).toBe(3); + }); + + it('should limit the page to the maximum allowed value if a page parameter is provided', () => { + expect(service.page(1000)).toBe(20); + }); + }); + + describe('perPage', () => { + it('should return the default perPage value if no perPage parameter is provided', () => { + expect(service.perPage()).toBe(20); + }); + + it('should return the 10 perPage', () => { + expect(service.perPage(10)).toBe(10); + }); + + it('should limit the perPage to the maximum allowed value if a perPage parameter is provided', () => { + expect(service.perPage(1000)).toBe(100); + }); + }); + + describe('order', () => { + it('should return the order as a key-value pair', () => { + expect( + service.order( + 'title', + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ['title', 'createdAt'] + ) + ).toEqual({ + title: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }); + }); + + it('should use the default orderBy, because of key-value not pair', () => { + expect( + service.order( + 'name', + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + ['title', 'createdAt'] + ) + ).toEqual({ + createdAt: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }); + }); + + it('should use the default orderBy and orderDirection values if no values are provided', () => { + expect(service.order()).toEqual({ + createdAt: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }); + }); + }); + + describe('search', () => { + it('should return a search object based on the provided searchValue and availableSearch fields', () => { + expect(service.search('John', ['firstName', 'lastName'])).toEqual( + DatabaseQueryOr([ + DatabaseQueryContain('firstName', 'John'), + DatabaseQueryContain('lastName', 'John'), + ]) + ); + }); + + it('should return undefined if no searchValue is provided, with undefined', () => { + expect( + service.search(undefined, ['firstName', 'lastName']) + ).toBeUndefined(); + }); + + it('should return undefined if no searchValue is provided, with empty string', () => { + expect( + service.search('', ['firstName', 'lastName']) + ).toBeUndefined(); + }); + }); + + describe('filterEqual', () => { + it('should return a filter object for an exact match on the field', () => { + expect(service.filterEqual('status', 'published')).toEqual( + DatabaseQueryEqual('status', 'published') + ); + }); + }); + + describe('filterNotEqual', () => { + it('should return a filter object for an exact not match on the field', () => { + expect(service.filterNotEqual('status', 'published')).toEqual( + DatabaseQueryNotEqual('status', 'published') + ); + }); + }); + + describe('filterContain', () => { + it('should return a filter object for a partial match on the field', () => { + expect(service.filterContain('title', 'John')).toEqual( + DatabaseQueryContain('title', 'John') + ); + }); + }); + + describe('filterContainFullMatch', () => { + it('should return a filter object for a full match on the field', () => { + expect(service.filterContainFullMatch('title', 'John')).toEqual( + DatabaseQueryContain('title', 'John', { fullWord: true }) + ); + }); + }); + + describe('filterIn', () => { + it('should return a filter object for a match in a list of possible values on the field', () => { + expect(service.filterIn('category', ['news', 'events'])).toEqual( + DatabaseQueryIn('category', ['news', 'events']) + ); + }); + }); + + describe('filterNin', () => { + it('should return a filter object for a match npt in a list of possible values on the field', () => { + expect(service.filterNin('category', ['news', 'events'])).toEqual( + DatabaseQueryNin('category', ['news', 'events']) + ); + }); + }); + + describe('filterDate', () => { + it('should return a filter object for a match on a date field', () => { + const date = new Date('2020-01-01T00:00:00Z'); + expect(service.filterDate('createdAt', date)).toEqual({ + createdAt: date, + }); + }); + }); +}); diff --git a/test/common/request/decorators/request.decorator.spec.ts b/test/common/request/decorators/request.decorator.spec.ts new file mode 100644 index 000000000..705c61c80 --- /dev/null +++ b/test/common/request/decorators/request.decorator.spec.ts @@ -0,0 +1,37 @@ +import { SetMetadata } from '@nestjs/common'; +import { + REQUEST_CUSTOM_TIMEOUT_META_KEY, + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, +} from 'src/common/request/constants/request.constant'; +import { RequestTimeout } from 'src/common/request/decorators/request.decorator'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + SetMetadata: jest.fn(), +})); + +describe('Request Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('RequestTimeout', () => { + it('Should return applyDecorators with property', async () => { + const result = RequestTimeout('2s'); + + expect(result).toBeTruthy(); + expect(SetMetadata).toHaveBeenCalledWith( + REQUEST_CUSTOM_TIMEOUT_META_KEY, + true + ); + expect(SetMetadata).toHaveBeenCalledWith( + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, + '2s' + ); + }); + + it('Should return applyDecorators', async () => { + expect(RequestTimeout('2s')).toBeTruthy(); + }); + }); +}); diff --git a/test/common/request/exceptions/request.validation.exception.spec.ts b/test/common/request/exceptions/request.validation.exception.spec.ts new file mode 100644 index 000000000..df557aa44 --- /dev/null +++ b/test/common/request/exceptions/request.validation.exception.spec.ts @@ -0,0 +1,42 @@ +import { HttpStatus } from '@nestjs/common'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; +import { RequestValidationException } from 'src/common/request/exceptions/request.validation.exception'; + +describe('RequestValidationException', () => { + let exception: RequestValidationException; + const errors = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + children: [], + constraints: { isEmail: 'email must be an email' }, + }, + ]; + + beforeEach(() => { + exception = new RequestValidationException(errors); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(exception).toBeDefined(); + }); + + it('should be RequestValidationException', () => { + expect(exception).toBeInstanceOf(RequestValidationException); + expect(exception.message).toEqual('request.validation'); + expect(exception.httpStatus).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(exception.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.VALIDATION + ); + expect(exception.errors).toEqual(errors); + }); +}); diff --git a/test/common/request/interceptors/request.timeout.interceptor.spec.ts b/test/common/request/interceptors/request.timeout.interceptor.spec.ts new file mode 100644 index 000000000..661586c5e --- /dev/null +++ b/test/common/request/interceptors/request.timeout.interceptor.spec.ts @@ -0,0 +1,226 @@ +import { + CallHandler, + ExecutionContext, + RequestTimeoutException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { createMock } from '@golevelup/ts-jest'; +import { throwError } from 'rxjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RequestTimeoutInterceptor } from 'src/common/request/interceptors/request.timeout.interceptor'; +import { marbles } from 'rxjs-marbles/jest'; +import { REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY } from 'src/common/request/constants/request.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/enums/request.status-code.enum'; + +describe('RequestTimeoutInterceptor', () => { + let interceptor: RequestTimeoutInterceptor; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + RequestTimeoutInterceptor, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + default: + return 2000; + } + }), + }, + }, + ], + }).compile(); + + interceptor = moduleRef.get( + RequestTimeoutInterceptor + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(interceptor).toBeDefined(); + }); + + it('should next if not http', async () => { + const executionContext = createMock({}); + const next: CallHandler = { + handle: jest.fn(), + }; + + interceptor.intercept(executionContext, next); + expect(next.handle).toHaveBeenCalled(); + }); + + describe('Default timeout', () => { + it( + 'should success', + marbles(m => { + const executionContext = createMock({ + getType: () => 'http', + }); + + const next: CallHandler = { + handle: () => m.cold('(e|)'), + }; + + const handler = interceptor.intercept(executionContext, next); + const expected = m.cold('(e|)'); + + m.expect(handler).toBeObservable(expected); + }) + ); + + it( + `should forward the error thrown`, + marbles(m => { + const executionContext = createMock({ + getType: () => 'http', + }); + + const error = new Error('something'); + const next = { + handle: () => throwError(() => error), + }; + + const handlerData$ = interceptor.intercept( + executionContext, + next + ); + + /** Marble emitting an error after `TIMEOUT`ms. */ + const expected$ = m.cold(`#`, undefined, error); + m.expect(handlerData$).toBeObservable(expected$); + }) + ); + + it( + 'should throw RequestTimeoutException (HTTP 408) error', + marbles(m => { + const executionContext = createMock({ + getType: () => 'http', + }); + + const next: CallHandler = { + handle: () => m.cold('3000ms |'), + }; + + const handler = interceptor.intercept(executionContext, next); + const expected = m.cold( + '2000ms #', + undefined, + new RequestTimeoutException({ + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, + message: 'http.clientError.requestTimeOut', + }) + ); + + m.expect(handler).toBeObservable(expected); + }) + ); + }); + + describe('Custom timeout', () => { + it( + 'should success', + marbles(m => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY: + return '1s'; + default: + return true; + } + } + ); + + const executionContext = createMock({ + getType: () => 'http', + }); + + const next: CallHandler = { + handle: () => m.cold('(e|)'), + }; + + const handler = interceptor.intercept(executionContext, next); + const expected = m.cold('(e|)'); + + m.expect(handler).toBeObservable(expected); + }) + ); + + it( + `should forward the error thrown`, + marbles(m => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY: + return '1s'; + default: + return true; + } + } + ); + + const executionContext = createMock({ + getType: () => 'http', + }); + + const error = new Error('something'); + const next = { + handle: () => throwError(() => error), + }; + + const handlerData$ = interceptor.intercept( + executionContext, + next + ); + + const expected$ = m.cold(`#`, undefined, error); + m.expect(handlerData$).toBeObservable(expected$); + }) + ); + + it( + 'should throw RequestTimeoutException (HTTP 408) error', + marbles(m => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY: + return '1s'; + default: + return true; + } + } + ); + + const executionContext = createMock({ + getType: () => 'http', + }); + + const next: CallHandler = { + handle: () => m.cold('3000ms |'), + }; + + const handler = interceptor.intercept(executionContext, next); + const expected = m.cold( + '1000ms #', + undefined, + new RequestTimeoutException({ + statusCode: ENUM_REQUEST_STATUS_CODE_ERROR.TIMEOUT, + message: 'http.clientError.requestTimeOut', + }) + ); + + m.expect(handler).toBeObservable(expected); + }) + ); + }); +}); diff --git a/test/common/request/pipes/request.required.pipe.spec.ts b/test/common/request/pipes/request.required.pipe.spec.ts new file mode 100644 index 000000000..557e10189 --- /dev/null +++ b/test/common/request/pipes/request.required.pipe.spec.ts @@ -0,0 +1,34 @@ +import { BadRequestException } from '@nestjs/common'; +import { RequestRequiredPipe } from 'src/common/request/pipes/request.required.pipe'; + +describe('RequestRequiredPipe', () => { + let pipe: RequestRequiredPipe; + + beforeEach(() => { + pipe = new RequestRequiredPipe(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + describe('transform', () => { + it('Should throw a BadRequestException when the field is undefined', async () => { + try { + await pipe.transform(undefined); + } catch (err: any) { + expect(err).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be successful calls', async () => { + const result = await pipe.transform('string'); + + expect(result).toBeDefined(); + }); + }); +}); diff --git a/test/common/response/decorators/response.decorator.spec.ts b/test/common/response/decorators/response.decorator.spec.ts new file mode 100644 index 000000000..4b83c3c4c --- /dev/null +++ b/test/common/response/decorators/response.decorator.spec.ts @@ -0,0 +1,202 @@ +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; +import { SetMetadata, UseInterceptors } from '@nestjs/common'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; +import { + RESPONSE_FILE_EXCEL_TYPE_META_KEY, + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { + Response, + ResponseFileExcel, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { ResponseFileExcelInterceptor } from 'src/common/response/interceptors/response.file.interceptor'; +import { ResponseInterceptor } from 'src/common/response/interceptors/response.interceptor'; +import { ResponsePagingInterceptor } from 'src/common/response/interceptors/response.paging.interceptor'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + SetMetadata: jest.fn(), + UseInterceptors: jest.fn(), +})); + +jest.mock('@nestjs/cache-manager', () => ({ + ...jest.requireActual('@nestjs/cache-manager'), + CacheKey: jest.fn(), + CacheTTL: jest.fn(), +})); + +describe('Response Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Response', () => { + it('Should return applyDecorators', async () => { + const result = Response('default'); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith(ResponseInterceptor); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + }); + + it('Should return applyDecorators with property', async () => { + const result = Response('default', { + messageProperties: { + test: 'aaa', + }, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith(ResponseInterceptor); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + { + test: 'aaa', + } + ); + }); + + it('Should return applyDecorators with cached', async () => { + const result = Response('default', { + cached: true, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith(ResponseInterceptor); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(UseInterceptors).toHaveBeenCalledWith(CacheInterceptor); + }); + + it('Should return applyDecorators with custom cached', async () => { + const result = Response('default', { + cached: { + key: 'default-test', + ttl: 60, + }, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith(ResponseInterceptor); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(UseInterceptors).toHaveBeenCalledWith(CacheInterceptor); + expect(CacheKey).toHaveBeenCalledWith('default-test'); + expect(CacheTTL).toHaveBeenCalledWith(60); + }); + }); + + describe('ResponsePaging', () => { + it('Should return applyDecorators', async () => { + const result = ResponsePaging('default'); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponsePagingInterceptor + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + }); + + it('Should return applyDecorators with property', async () => { + const result = ResponsePaging('default', { + messageProperties: { + test: 'aaa', + }, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponsePagingInterceptor + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + { + test: 'aaa', + } + ); + }); + + it('Should return applyDecorators with cached', async () => { + const result = ResponsePaging('default', { + cached: true, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponsePagingInterceptor + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(UseInterceptors).toHaveBeenCalledWith(CacheInterceptor); + }); + + it('Should return applyDecorators with custom cached', async () => { + const result = ResponsePaging('default', { + cached: { + key: 'default-test', + ttl: 60, + }, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponsePagingInterceptor + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_MESSAGE_PATH_META_KEY, + 'default' + ); + expect(UseInterceptors).toHaveBeenCalledWith(CacheInterceptor); + expect(CacheKey).toHaveBeenCalledWith('default-test'); + expect(CacheTTL).toHaveBeenCalledWith(60); + }); + }); + + describe('ResponseFileExcel', () => { + it('Should return applyDecorators', async () => { + const result = ResponseFileExcel(); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponseFileExcelInterceptor + ); + }); + + it('Should return applyDecorators with property', async () => { + const result = ResponseFileExcel({ + type: ENUM_HELPER_FILE_EXCEL_TYPE.CSV, + }); + + expect(result).toBeTruthy(); + expect(UseInterceptors).toHaveBeenCalledWith( + ResponseFileExcelInterceptor + ); + expect(SetMetadata).toHaveBeenCalledWith( + RESPONSE_FILE_EXCEL_TYPE_META_KEY, + ENUM_HELPER_FILE_EXCEL_TYPE.CSV + ); + }); + }); +}); diff --git a/test/common/response/dtos/reponse.paging.dto.spec.ts b/test/common/response/dtos/reponse.paging.dto.spec.ts new file mode 100644 index 000000000..b422137e0 --- /dev/null +++ b/test/common/response/dtos/reponse.paging.dto.spec.ts @@ -0,0 +1,73 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { ResponsePagingDto } from 'src/common/response/dtos/response.paging.dto'; + +describe('ResponsePagingDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: ResponsePagingDto = { + _metadata: { + language: faker.helpers.arrayElement( + Object.values(ENUM_MESSAGE_LANGUAGE) + ), + path: '/path/test', + repoVersion: '1.0.0', + timestamp: faker.date.anytime().valueOf(), + timezone: 'Asia/Jakarta', + version: '1', + pagination: { + search: 'testSearch', + filters: { + status: 'statusTest', + }, + page: 1, + perPage: 10, + orderBy: 'createdAt', + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + availableSearch: [], + availableOrderBy: ['createAt'], + availableOrderDirection: Object.values( + ENUM_PAGINATION_ORDER_DIRECTION_TYPE + ), + total: 5, + totalPage: 1, + }, + }, + message: 'testMessage', + statusCode: 200, + data: [], + }; + + const dto = plainToInstance(ResponsePagingDto, response); + + expect(dto).toBeInstanceOf(ResponsePagingDto); + expect(dto.data).toBeDefined(); + expect(dto.data).toEqual([]); + expect(dto.statusCode).toBeDefined(); + expect(dto.message).toBeDefined(); + expect(dto._metadata).toBeDefined(); + expect(dto._metadata.language).toBeDefined(); + expect(dto._metadata.path).toBeDefined(); + expect(dto._metadata.repoVersion).toBeDefined(); + expect(dto._metadata.timestamp).toBeDefined(); + expect(dto._metadata.timezone).toBeDefined(); + expect(dto._metadata.version).toBeDefined(); + expect(dto._metadata.pagination).toBeDefined(); + expect(dto._metadata.pagination.search).toBeDefined(); + expect(dto._metadata.pagination.filters).toBeDefined(); + expect(dto._metadata.pagination.page).toBeDefined(); + expect(dto._metadata.pagination.perPage).toBeDefined(); + expect(dto._metadata.pagination.orderBy).toBeDefined(); + expect(dto._metadata.pagination.orderDirection).toBeDefined(); + expect(dto._metadata.pagination.availableSearch).toBeDefined(); + expect(dto._metadata.pagination.availableOrderBy).toBeDefined(); + expect(dto._metadata.pagination.availableOrderDirection).toBeDefined(); + expect(dto._metadata.pagination.total).toBeDefined(); + expect(dto._metadata.pagination.totalPage).toBeDefined(); + }); +}); diff --git a/test/common/response/dtos/response.dto.spec.ts b/test/common/response/dtos/response.dto.spec.ts new file mode 100644 index 000000000..ab8d89831 --- /dev/null +++ b/test/common/response/dtos/response.dto.spec.ts @@ -0,0 +1,75 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; +import { ResponseDto } from 'src/common/response/dtos/response.dto'; + +describe('ResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const response: ResponseDto = { + _metadata: { + language: faker.helpers.arrayElement( + Object.values(ENUM_MESSAGE_LANGUAGE) + ), + path: '/path/test', + repoVersion: '1.0.0', + timestamp: faker.date.anytime().valueOf(), + timezone: 'Asia/Jakarta', + version: '1', + }, + message: 'testMessage', + statusCode: 200, + }; + + const dto = plainToInstance(ResponseDto, response); + + expect(dto).toBeInstanceOf(ResponseDto); + expect(dto.data).toBeUndefined(); + expect(dto.statusCode).toBeDefined(); + expect(dto.message).toBeDefined(); + expect(dto._metadata).toBeDefined(); + expect(dto._metadata.language).toBeDefined(); + expect(dto._metadata.path).toBeDefined(); + expect(dto._metadata.repoVersion).toBeDefined(); + expect(dto._metadata.timestamp).toBeDefined(); + expect(dto._metadata.timezone).toBeDefined(); + expect(dto._metadata.version).toBeDefined(); + }); + + it('Should be successful calls with data', () => { + const response: ResponseDto = { + _metadata: { + language: faker.helpers.arrayElement( + Object.values(ENUM_MESSAGE_LANGUAGE) + ), + path: '/path/test', + repoVersion: '1.0.0', + timestamp: faker.date.anytime().valueOf(), + timezone: 'Asia/Jakarta', + version: '1', + }, + message: 'testMessage', + statusCode: 200, + data: { + test: 'appName', + }, + }; + + const dto = plainToInstance(ResponseDto, response); + + expect(dto).toBeInstanceOf(ResponseDto); + expect(dto.data).toBeDefined(); + expect(dto.statusCode).toBeDefined(); + expect(dto.message).toBeDefined(); + expect(dto._metadata).toBeDefined(); + expect(dto._metadata.language).toBeDefined(); + expect(dto._metadata.path).toBeDefined(); + expect(dto._metadata.repoVersion).toBeDefined(); + expect(dto._metadata.timestamp).toBeDefined(); + expect(dto._metadata.timezone).toBeDefined(); + expect(dto._metadata.version).toBeDefined(); + }); +}); diff --git a/test/common/response/interceptors/response.file.interceptor.spec.ts b/test/common/response/interceptors/response.file.interceptor.spec.ts new file mode 100644 index 000000000..a4773ffe8 --- /dev/null +++ b/test/common/response/interceptors/response.file.interceptor.spec.ts @@ -0,0 +1,272 @@ +import { createMock } from '@golevelup/ts-jest'; +import { CallHandler, ExecutionContext, StreamableFile } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { lastValueFrom, of } from 'rxjs'; +import { FileService } from 'src/common/file/services/file.service'; +import { ENUM_HELPER_FILE_EXCEL_TYPE } from 'src/common/helper/enums/helper.enum'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { RESPONSE_FILE_EXCEL_TYPE_META_KEY } from 'src/common/response/constants/response.constant'; +import { ResponseFileExcelInterceptor } from 'src/common/response/interceptors/response.file.interceptor'; + +describe('ResponseFileExcelInterceptor', () => { + let interceptor: ResponseFileExcelInterceptor; + + const mockHelperDateService = { + createTimestamp: jest.fn(), + }; + + const mockFileService = { + writeCsv: jest.fn().mockReturnValue(Buffer.from('123456')), + writeExcel: jest.fn().mockReturnValue(Buffer.from('123456')), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + ResponseFileExcelInterceptor, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { + provide: FileService, + useValue: mockFileService, + }, + ], + }).compile(); + + interceptor = moduleRef.get( + ResponseFileExcelInterceptor + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(interceptor).toBeDefined(); + }); + + it('should next if not http', async () => { + const executionContext = createMock({}); + const next: CallHandler = { + handle: jest.fn(), + }; + + interceptor.intercept(executionContext, next); + expect(next.handle).toHaveBeenCalled(); + }); + + it('should throw an error if data is undefined', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_FILE_EXCEL_TYPE_META_KEY: + return ENUM_HELPER_FILE_EXCEL_TYPE.CSV; + default: + return null; + } + } + ); + + const res = { headers: {}, data: [] } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => of(undefined), + }; + + try { + await lastValueFrom(interceptor.intercept(executionContext, next)); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe( + 'ResponseFileExcel must instanceof IResponseFileExcel' + ); + } + }); + + it('should throw an error if data is not array', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_FILE_EXCEL_TYPE_META_KEY: + return ENUM_HELPER_FILE_EXCEL_TYPE.CSV; + default: + return null; + } + } + ); + + const res = { headers: {}, data: [] } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + data: {}, + }), + }; + + try { + await lastValueFrom(interceptor.intercept(executionContext, next)); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Field data must in array'); + } + }); + + it('should return file CSV as response', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_FILE_EXCEL_TYPE_META_KEY: + return ENUM_HELPER_FILE_EXCEL_TYPE.CSV; + default: + return null; + } + } + ); + + const res = { headers: {}, data: [] } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + statusCode: 200, + data: [], + }), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeInstanceOf(StreamableFile); + }); + + it('should return file XLSX as response', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_FILE_EXCEL_TYPE_META_KEY: + return ENUM_HELPER_FILE_EXCEL_TYPE.XLSX; + default: + return null; + } + } + ); + + const res = { headers: {}, data: [] } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + statusCode: 200, + data: [], + }), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeInstanceOf(StreamableFile); + }); +}); diff --git a/test/common/response/interceptors/response.interceptor.spec.ts b/test/common/response/interceptors/response.interceptor.spec.ts new file mode 100644 index 000000000..c628c468c --- /dev/null +++ b/test/common/response/interceptors/response.interceptor.spec.ts @@ -0,0 +1,206 @@ +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { CallHandler, ExecutionContext } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { lastValueFrom, of } from 'rxjs'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { MessageService } from 'src/common/message/services/message.service'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { ResponseInterceptor } from 'src/common/response/interceptors/response.interceptor'; + +describe('ResponseInterceptor', () => { + let interceptor: ResponseInterceptor; + + const mockHelperDateService = { + createTimestamp: jest.fn(), + }; + + const mockMessageService = { + getLanguage: jest.fn(), + setMessage: jest.fn().mockReturnValue(''), + }; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.versioning.version': + return '1'; + case 'app.repoVersion': + return '1.0.0'; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + ResponseInterceptor, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { + provide: MessageService, + useValue: mockMessageService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + interceptor = moduleRef.get(ResponseInterceptor); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(interceptor).toBeDefined(); + }); + + it('should next if not http', async () => { + const executionContext = createMock({}); + const next: CallHandler = { + handle: jest.fn(), + }; + + interceptor.intercept(executionContext, next); + expect(next.handle).toHaveBeenCalled(); + }); + + describe('should return response as ResponseDto', () => { + it('with data', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return true; + } + } + ); + + const res = { + headers: {}, + data: [], + statusCode: 200, + } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + data: { + _id: faker.string.uuid(), + }, + }), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeDefined(); + expect(result.statusCode).toBeDefined(); + expect(result.message).toBeDefined(); + expect(result._metadata).toBeDefined(); + expect(result.data).toBeDefined(); + expect(result.data._id).toBeDefined(); + }); + + it('without data', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return true; + } + } + ); + + const res = { + headers: {}, + data: [], + statusCode: 200, + } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => of(undefined), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeDefined(); + expect(result.statusCode).toBeDefined(); + expect(result.message).toBeDefined(); + expect(result._metadata).toBeDefined(); + expect(result.data).toBeUndefined(); + }); + }); +}); diff --git a/test/common/response/interceptors/response.paging.interceptor.spec.ts b/test/common/response/interceptors/response.paging.interceptor.spec.ts new file mode 100644 index 000000000..e11b90bcc --- /dev/null +++ b/test/common/response/interceptors/response.paging.interceptor.spec.ts @@ -0,0 +1,401 @@ +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { CallHandler, ExecutionContext } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { lastValueFrom, of } from 'rxjs'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/enums/message.enum'; +import { MessageService } from 'src/common/message/services/message.service'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { ResponsePagingInterceptor } from 'src/common/response/interceptors/response.paging.interceptor'; + +describe('ResponsePagingInterceptor', () => { + let interceptor: ResponsePagingInterceptor; + + const mockHelperDateService = { + createTimestamp: jest.fn(), + }; + + const mockMessageService = { + getLanguage: jest.fn().mockReturnValue(ENUM_MESSAGE_LANGUAGE.EN), + setMessage: jest.fn().mockReturnValue('default message'), + }; + + const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'app.versioning.version': + return '1'; + case 'app.repoVersion': + return '1.0.0'; + default: + return null; + } + }), + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [ + ResponsePagingInterceptor, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { + provide: MessageService, + useValue: mockMessageService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + interceptor = moduleRef.get( + ResponsePagingInterceptor + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(interceptor).toBeDefined(); + }); + + it('should next if not http', async () => { + const executionContext = createMock({}); + const next: CallHandler = { + handle: jest.fn(), + }; + + interceptor.intercept(executionContext, next); + expect(next.handle).toHaveBeenCalled(); + }); + + it('should throw an error if data is undefined', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return null; + } + } + ); + + const res = { headers: {}, data: undefined } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => of(undefined), + }; + + try { + await lastValueFrom(interceptor.intercept(executionContext, next)); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe( + 'ResponsePaging must instanceof IResponsePaging' + ); + } + }); + + it('should throw an error if data is not array', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return null; + } + } + ); + + const res = { headers: {}, data: '' } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + body: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + data: {}, + }), + }; + + try { + await lastValueFrom(interceptor.intercept(executionContext, next)); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe( + 'Field data must in array and can not be empty' + ); + } + }); + + describe('should return response as ResponsePagingDto', () => { + it('with data without no filters and no search', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return null; + } + } + ); + + const res = { + headers: {}, + data: [], + statusCode: 200, + } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + __pagination: { + search: undefined, + filters: undefined, + page: 1, + perPage: 10, + orderBy: 'createdAt', + orderDirection: + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + availableSearch: [], + availableOrderBy: ['createAt'], + availableOrderDirection: Object.values( + ENUM_PAGINATION_ORDER_DIRECTION_TYPE + ), + }, + body: {}, + query: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + _pagination: { + total: 10, + totalPage: 1, + }, + data: [ + { + _id: faker.string.uuid(), + }, + ], + }), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeDefined(); + expect(result.statusCode).toBeDefined(); + expect(result.message).toBeDefined(); + expect(result._metadata).toBeDefined(); + expect(result._metadata.pagination).toBeDefined(); + expect(result._metadata.pagination.search).toBeUndefined(); + expect(result._metadata.pagination.filters).toBeUndefined(); + expect(result._metadata.pagination.page).toBeDefined(); + expect(result._metadata.pagination.perPage).toBeDefined(); + expect(result._metadata.pagination.orderBy).toBeDefined(); + expect(result._metadata.pagination.orderDirection).toBeDefined(); + expect(result._metadata.pagination.availableSearch).toBeDefined(); + expect(result._metadata.pagination.availableOrderBy).toBeDefined(); + expect( + result._metadata.pagination.availableOrderDirection + ).toBeDefined(); + expect(result._metadata.pagination.total).toBeDefined(); + expect(result._metadata.pagination.totalPage).toBeDefined(); + expect(result.data).toBeDefined(); + expect(result.data.length).toBe(1); + expect(result.data[0]._id).toBeDefined(); + }); + + it('with data with filters and search', async () => { + jest.spyOn(interceptor['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case RESPONSE_MESSAGE_PATH_META_KEY: + return 'default'; + case RESPONSE_MESSAGE_PROPERTIES_META_KEY: + return undefined; + default: + return null; + } + } + ); + + const res = { + headers: {}, + data: [], + statusCode: 200, + } as unknown as Response; + const headers = {} as Record; + res.json = jest.fn(); + res.status = jest.fn(() => res); + + res.setHeader = jest + .fn() + .mockImplementation((key: string, value: string) => { + headers[key] = value; + + return res; + }); + res.getHeader = jest + .fn() + .mockImplementation((key: string) => headers[key]); + + const executionContext = createMock({ + getType: () => 'http', + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + __pagination: { + search: 'test', + filters: { + status: 'statusTest', + }, + page: 1, + perPage: 10, + orderBy: 'createdAt', + orderDirection: + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + availableSearch: [], + availableOrderBy: ['createAt'], + availableOrderDirection: Object.values( + ENUM_PAGINATION_ORDER_DIRECTION_TYPE + ), + }, + body: {}, + query: {}, + }), + getResponse: jest.fn().mockReturnValue(res), + }), + }); + + const next: CallHandler = { + handle: () => + of({ + _pagination: { + total: 10, + totalPage: 1, + }, + data: [ + { + _id: faker.string.uuid(), + }, + ], + }), + }; + + const result = await lastValueFrom( + interceptor.intercept(executionContext, next) + ); + + expect(result).toBeDefined(); + expect(result.statusCode).toBeDefined(); + expect(result.message).toBeDefined(); + expect(result._metadata).toBeDefined(); + expect(result._metadata.pagination).toBeDefined(); + expect(result._metadata.pagination.search).toBeDefined(); + expect(result._metadata.pagination.filters).toBeDefined(); + expect(result._metadata.pagination.page).toBeDefined(); + expect(result._metadata.pagination.perPage).toBeDefined(); + expect(result._metadata.pagination.orderBy).toBeDefined(); + expect(result._metadata.pagination.orderDirection).toBeDefined(); + expect(result._metadata.pagination.availableSearch).toBeDefined(); + expect(result._metadata.pagination.availableOrderBy).toBeDefined(); + expect( + result._metadata.pagination.availableOrderDirection + ).toBeDefined(); + expect(result._metadata.pagination.total).toBeDefined(); + expect(result._metadata.pagination.totalPage).toBeDefined(); + expect(result.data).toBeDefined(); + expect(result.data.length).toBe(1); + expect(result.data[0]._id).toBeDefined(); + }); + }); +}); diff --git a/test/jest.json b/test/jest.json index 9cd60a4a4..d2e3f6ab5 100644 --- a/test/jest.json +++ b/test/jest.json @@ -6,7 +6,37 @@ "testMatch": ["/test/**/*.spec.ts"], "collectCoverage": true, "coverageDirectory": "coverage", - "collectCoverageFrom": [], + "collectCoverageFrom": [ + "/src/app/dtos/*.dto.ts", + "/src/app/filters/*.filter.ts", + "/src/app/middlewares/*.middleware.ts", + + "/src/common/**/services/**/*.service.ts", + "/src/common/**/pipes/**/*.pipe.ts", + "/src/common/**/guards/**/*.guard.ts", + "/src/common/**/guards/**/*.strategy.ts", + "/src/common/**/interceptors/**/*.interceptor.ts", + "/src/common/**/dtos/**/*.dto.ts", + "/src/common/**/decorators/**/*.decorator.ts", + "/src/common/**/exceptions/**/*.exception.ts", + "/src/common/**/filters/**/*.filter.ts", + "/src/common/**/middlewares/**/*.middleware.ts", + "/src/common/**/indicators/**/*.indicator.ts", + "/src/common/**/factories/**/*.factory.ts", + + "/src/modules/{api-key,auth,aws,country,policy}/services/**/*.service.ts", + "/src/modules/{api-key,auth,aws,country,policy}/pipes/**/*.pipe.ts", + "/src/modules/{api-key,auth,aws,country,policy}/guards/**/*.guard.ts", + "/src/modules/{api-key,auth,aws,country,policy}/guards/**/*.strategy.ts", + "/src/modules/{api-key,auth,aws,country,policy}/interceptors/**/*.interceptor.ts", + "/src/modules/{api-key,auth,aws,country,policy}/dtos/**/*.dto.ts", + "/src/modules/{api-key,auth,aws,country,policy}/decorators/**/*.decorator.ts", + "/src/modules/{api-key,auth,aws,country,policy}/exceptions/**/*.exception.ts", + "/src/modules/{api-key,auth,aws,country,policy}/filters/**/*.filter.ts", + "/src/modules/{api-key,auth,aws,country,policy}/middlewares/**/*.middleware.ts", + "/src/modules/{api-key,auth,aws,country,policy}/indicators/**/*.indicator.ts", + "/src/modules/{api-key,auth,aws,country,policy}/factories/**/*.factory.ts" + ], "coverageThreshold": { "global": { "branches": 100, diff --git a/test/modules/api-key/decorators/api-key.decorator.spec.ts b/test/modules/api-key/decorators/api-key.decorator.spec.ts new file mode 100644 index 000000000..0144cc2c1 --- /dev/null +++ b/test/modules/api-key/decorators/api-key.decorator.spec.ts @@ -0,0 +1,128 @@ +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { ExecutionContext, SetMetadata, UseGuards } from '@nestjs/common'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { API_KEY_X_TYPE_META_KEY } from 'src/modules/api-key/constants/api-key.constant'; +import { + ApiKeyPayload, + ApiKeyProtected, + ApiKeySystemProtected, +} from 'src/modules/api-key/decorators/api-key.decorator'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; +import { ApiKeyXApiKeyGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard'; +import { ApiKeyXApiKeyTypeGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + UseGuards: jest.fn(), + SetMetadata: jest.fn(), +})); + +/* eslint-disable */ +function getParamDecoratorFactory(decorator: any) { + class Test { + public test(@decorator() value) {} + } + + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test'); + return args[Object.keys(args)[0]].factory; +} +/* eslint-enable */ + +describe('ApiKey Decorator', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('ApiKeyPayload', () => { + it('Should return apiKey', () => { + const apiKey = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + hash: faker.string.alpha(20), + isActive: true, + startDate: faker.date.past(), + endDate: faker.date.future(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + }; + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + apiKey, + }), + }), + }); + + const decorator = getParamDecoratorFactory(ApiKeyPayload); + + const result = decorator(null, executionContext); + expect(result).toBeTruthy(); + expect(result).toEqual(apiKey); + }); + + it('Should return apiKey id', () => { + const apiKey = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + hash: faker.string.alpha(20), + isActive: true, + startDate: faker.date.past(), + endDate: faker.date.future(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + }; + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + apiKey, + }), + }), + }); + + const decorator = getParamDecoratorFactory(ApiKeyPayload); + + const result = decorator('_id', executionContext); + expect(result).toBeTruthy(); + expect(result).toEqual(apiKey._id); + }); + }); + + describe('ApiKeySystemProtected', () => { + it('should create a valid ApiKeySystemProtected decorator', () => { + const result = ApiKeySystemProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith( + ApiKeyXApiKeyGuard, + ApiKeyXApiKeyTypeGuard + ); + expect(SetMetadata).toHaveBeenCalledWith(API_KEY_X_TYPE_META_KEY, [ + ENUM_API_KEY_TYPE.SYSTEM, + ]); + }); + }); + + describe('ApiKeyProtected', () => { + it('should create a valid ApiKeyProtected decorator', () => { + const result = ApiKeyProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith( + ApiKeyXApiKeyGuard, + ApiKeyXApiKeyTypeGuard + ); + expect(SetMetadata).toHaveBeenCalledWith(API_KEY_X_TYPE_META_KEY, [ + ENUM_API_KEY_TYPE.DEFAULT, + ]); + }); + }); +}); diff --git a/test/modules/api-key/dtos/api-key.payload.dto.spec.ts b/test/modules/api-key/dtos/api-key.payload.dto.spec.ts new file mode 100644 index 000000000..2e0bf98a4 --- /dev/null +++ b/test/modules/api-key/dtos/api-key.payload.dto.spec.ts @@ -0,0 +1,20 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { ApiKeyPayloadDto } from 'src/modules/api-key/dtos/api-key.payload.dto'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; + +describe('ApiKeyPayloadDto', () => { + it('should create a valid ApiKeyPayloadDto object', () => { + const mockApiKeyPayload: ApiKeyPayloadDto = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + }; + + const dto = plainToInstance(ApiKeyPayloadDto, mockApiKeyPayload); + + expect(dto).toBeInstanceOf(ApiKeyPayloadDto); + }); +}); diff --git a/test/modules/api-key/dtos/request/api-key.create.request.dto.spec.ts b/test/modules/api-key/dtos/request/api-key.create.request.dto.spec.ts new file mode 100644 index 000000000..b31882e17 --- /dev/null +++ b/test/modules/api-key/dtos/request/api-key.create.request.dto.spec.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker'; +import { ApiKeyCreateRawRequestDto } from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { plainToInstance } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; + +describe('ApiKeyCreateRawRequestDto', () => { + it('should create a valid', () => { + const mockApiKeyCreateRawRequest: Record = { + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alphanumeric(10), + secret: faker.string.alphanumeric(20), + name: faker.company.name(), + startDate: faker.date.past(), + endDate: faker.date.future(), + }; + + const dto = plainToInstance( + ApiKeyCreateRawRequestDto, + mockApiKeyCreateRawRequest + ); + + expect(dto).toBeInstanceOf(ApiKeyCreateRawRequestDto); + }); + + it('should return errors instance', async () => { + const mockApiKeyCreateRawRequest: Record = { + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alphanumeric(10), + secret: faker.string.alphanumeric(20), + name: 1234, + }; + + const dto = plainToInstance( + ApiKeyCreateRawRequestDto, + mockApiKeyCreateRawRequest + ); + + const errors: ValidationError[] = await validate(dto); + + expect(dto).toBeInstanceOf(ApiKeyCreateRawRequestDto); + expect(Array.isArray(errors)).toEqual(true); + expect(errors.length).toEqual(1); + expect(errors.every(e => e instanceof ValidationError)).toEqual(true); + }); +}); diff --git a/test/modules/api-key/dtos/request/api-key.update-date.request.dto.spec.ts b/test/modules/api-key/dtos/request/api-key.update-date.request.dto.spec.ts new file mode 100644 index 000000000..553a6da4f --- /dev/null +++ b/test/modules/api-key/dtos/request/api-key.update-date.request.dto.spec.ts @@ -0,0 +1,44 @@ +import { ApiKeyUpdateDateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update-date.request.dto'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; + +describe('ApiKeyCreateRawRequestDto', () => { + it('should create a valid', () => { + const startDate = faker.date.recent(); + const endDate = new Date(startDate.getTime() + 1000 * 60 * 60 * 24); + + const mockApiKeyUpdateDateRequest: Record = { + startDate, + endDate, + }; + + const dto = plainToInstance( + ApiKeyUpdateDateRequestDto, + mockApiKeyUpdateDateRequest + ); + + expect(dto).toBeInstanceOf(ApiKeyUpdateDateRequestDto); + }); + + it('should return errors instance', async () => { + const startDate = faker.date.recent(); + + const mockApiKeyUpdateDateRequest: Record = { + startDate, + endDate: 1234, + }; + + const dto = plainToInstance( + ApiKeyUpdateDateRequestDto, + mockApiKeyUpdateDateRequest + ); + + const errors: ValidationError[] = await validate(dto); + + expect(dto).toBeInstanceOf(ApiKeyUpdateDateRequestDto); + expect(Array.isArray(errors)).toEqual(true); + expect(errors.length).toEqual(2); + expect(errors.every(e => e instanceof ValidationError)).toEqual(true); + }); +}); diff --git a/test/modules/api-key/dtos/request/api-key.update.request.dto.spec.ts b/test/modules/api-key/dtos/request/api-key.update.request.dto.spec.ts new file mode 100644 index 000000000..61f0bed95 --- /dev/null +++ b/test/modules/api-key/dtos/request/api-key.update.request.dto.spec.ts @@ -0,0 +1,37 @@ +import { ApiKeyUpdateRequestDto } from 'src/modules/api-key/dtos/request/api-key.update.request.dto'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; + +describe('ApiKeyUpdateRequestDto', () => { + it('should create a valid', () => { + const mockApiKeyUpdateRequest: Record = { + name: faker.company.name(), + }; + + const dto = plainToInstance( + ApiKeyUpdateRequestDto, + mockApiKeyUpdateRequest + ); + + expect(dto).toBeInstanceOf(ApiKeyUpdateRequestDto); + }); + + it('should return errors instance', async () => { + const mockApiKeyUpdateRequest: Record = { + name: 123, + }; + + const dto = plainToInstance( + ApiKeyUpdateRequestDto, + mockApiKeyUpdateRequest + ); + + const errors: ValidationError[] = await validate(dto); + + expect(dto).toBeInstanceOf(ApiKeyUpdateRequestDto); + expect(Array.isArray(errors)).toEqual(true); + expect(errors.length).toEqual(1); + expect(errors.every(e => e instanceof ValidationError)).toEqual(true); + }); +}); diff --git a/test/modules/api-key/dtos/response/api-key.create.dto.spec.ts b/test/modules/api-key/dtos/response/api-key.create.dto.spec.ts new file mode 100644 index 000000000..f7d534e77 --- /dev/null +++ b/test/modules/api-key/dtos/response/api-key.create.dto.spec.ts @@ -0,0 +1,21 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ApiKeyCreateResponseDto } from 'src/modules/api-key/dtos/response/api-key.create.dto'; + +describe('ApiKeyCreateResponseDto', () => { + it('should create a valid ApiKeyCreateResponseDto object', () => { + const mockApiKeyCreateResponse: ApiKeyCreateResponseDto = { + _id: faker.string.uuid(), + key: faker.string.alpha(15), + secret: faker.string.alpha(20), + }; + + const dto = plainToInstance( + ApiKeyCreateResponseDto, + mockApiKeyCreateResponse + ); + + expect(dto).toBeInstanceOf(ApiKeyCreateResponseDto); + }); +}); diff --git a/test/modules/api-key/dtos/response/api-key.get.response.dto.spec.ts b/test/modules/api-key/dtos/response/api-key.get.response.dto.spec.ts new file mode 100644 index 000000000..9612aa95f --- /dev/null +++ b/test/modules/api-key/dtos/response/api-key.get.response.dto.spec.ts @@ -0,0 +1,31 @@ +import 'reflect-metadata'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; + +describe('ApiKeyGetResponseDto', () => { + it('should create a valid ApiKeyGetResponseDto object', () => { + const mockApiKeyGetResponse: ApiKeyGetResponseDto = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + hash: faker.string.alpha(20), + isActive: true, + startDate: faker.date.past(), + endDate: faker.date.future(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + deleted: false, + }; + + const dto = plainToInstance( + ApiKeyGetResponseDto, + mockApiKeyGetResponse + ); + + expect(dto).toBeInstanceOf(ApiKeyGetResponseDto); + }); +}); diff --git a/test/modules/api-key/dtos/response/api-key.list.response.dto.spec.ts b/test/modules/api-key/dtos/response/api-key.list.response.dto.spec.ts new file mode 100644 index 000000000..e327d091a --- /dev/null +++ b/test/modules/api-key/dtos/response/api-key.list.response.dto.spec.ts @@ -0,0 +1,31 @@ +import 'reflect-metadata'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; + +describe('ApiKeyListResponseDto', () => { + it('should create a valid ApiKeyListResponseDto object', () => { + const mockApiKeyListResponse: ApiKeyListResponseDto = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + hash: faker.string.alpha(20), + isActive: true, + startDate: faker.date.past(), + endDate: faker.date.future(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + deleted: false, + }; + + const dto = plainToInstance( + ApiKeyListResponseDto, + mockApiKeyListResponse + ); + + expect(dto).toBeInstanceOf(ApiKeyListResponseDto); + }); +}); diff --git a/test/modules/api-key/dtos/response/api-key.reset.dto.spec.ts b/test/modules/api-key/dtos/response/api-key.reset.dto.spec.ts new file mode 100644 index 000000000..69a070434 --- /dev/null +++ b/test/modules/api-key/dtos/response/api-key.reset.dto.spec.ts @@ -0,0 +1,21 @@ +import 'reflect-metadata'; +import { ApiKeyResetResponseDto } from 'src/modules/api-key/dtos/response/api-key.reset.dto'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; + +describe('ApiKeyResetResponseDto', () => { + it('should create a valid ApiKeyResetResponseDto object', () => { + const mockApiKeyResetResponse: ApiKeyResetResponseDto = { + _id: faker.string.uuid(), + key: faker.string.alpha(15), + secret: faker.string.alpha(20), + }; + + const dto = plainToInstance( + ApiKeyResetResponseDto, + mockApiKeyResetResponse + ); + + expect(dto).toBeInstanceOf(ApiKeyResetResponseDto); + }); +}); diff --git a/test/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.spec.ts b/test/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.spec.ts new file mode 100644 index 000000000..c4f652025 --- /dev/null +++ b/test/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.spec.ts @@ -0,0 +1,129 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UnauthorizedException, ForbiddenException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiKeyXApiKeyGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; + +describe('ApiKeyXApiKeyGuard', () => { + let guard: ApiKeyXApiKeyGuard; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApiKeyXApiKeyGuard, + { + provide: AuthGuard('x-api-key'), + useValue: { + canActivate: jest.fn(), + handleRequest: jest.fn(), + prototype: { + canActivate: jest.fn(), + }, + }, + }, + ], + }).compile(); + + guard = module.get(ApiKeyXApiKeyGuard); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should throw UnauthorizedException if apiKey is missing', () => { + const err = null; + const apiKey = {}; + const info = { message: 'Missing Api Key' }; + + try { + guard.handleRequest(err, apiKey, info as any); + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_REQUIRED, + message: 'apiKey.error.xApiKey.required', + }); + } + }); + + it('should throw ForbiddenException if apiKey not found', () => { + const err = new Error( + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND.toString() + ); + const apiKey = {}; + const info = null; + + try { + guard.handleRequest(err, apiKey, info); + } catch (error) { + expect(error).toBeInstanceOf(ForbiddenException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND, + message: 'apiKey.error.xApiKey.notFound', + }); + } + }); + + it('should throw ForbiddenException if apiKey is inactive', () => { + const err = new Error( + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE.toString() + ); + const apiKey = {}; + const info = null; + + try { + guard.handleRequest(err, apiKey, info); + } catch (error) { + expect(error).toBeInstanceOf(ForbiddenException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE, + message: 'apiKey.error.xApiKey.inactive', + }); + } + }); + + it('should throw ForbiddenException if apiKey is expired', () => { + const err = new Error( + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED.toString() + ); + const apiKey = {}; + const info = null; + + try { + guard.handleRequest(err, apiKey, info); + } catch (error) { + expect(error).toBeInstanceOf(ForbiddenException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED, + message: 'apiKey.error.xApiKey.expired', + }); + } + }); + + it('should throw UnauthorizedException if apiKey is invalid', () => { + const err = new Error(); + const apiKey = {}; + const info = null; + + try { + guard.handleRequest(err, apiKey, info); + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID, + message: 'apiKey.error.xApiKey.invalid', + }); + } + }); + + it('should return the apiKey if valid', () => { + const err = null; + const apiKey = { key: 'valid-api-key' }; + const info = null; + + const result = guard.handleRequest(err, apiKey, info); + + expect(result).toEqual(apiKey); + }); +}); diff --git a/test/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.spec.ts b/test/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.spec.ts new file mode 100644 index 000000000..45361b888 --- /dev/null +++ b/test/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard.spec.ts @@ -0,0 +1,94 @@ +import { Test } from '@nestjs/testing'; +import { ApiKeyXApiKeyTypeGuard } from 'src/modules/api-key/guards/x-api-key/api-key.x-api-key.type.guard'; +import { Reflector } from '@nestjs/core'; +import { BadRequestException, ExecutionContext } from '@nestjs/common'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; + +const mockReflector = { + getAllAndOverride: jest.fn(), +}; + +function createMockExecutionContext( + request: Partial = {} +): ExecutionContext { + return { + switchToHttp: () => ({ + getRequest: () => request, + }), + getClass: jest.fn(), + getHandler: jest.fn(), + } as unknown as ExecutionContext; +} + +describe('ApiKeyXApiKeyTypeGuard', () => { + let guard: ApiKeyXApiKeyTypeGuard; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + ApiKeyXApiKeyTypeGuard, + { + provide: Reflector, + useValue: mockReflector, + }, + ], + }).compile(); + + guard = module.get(ApiKeyXApiKeyTypeGuard); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + describe('canActivate', () => { + it('should return true if no required types are specified', async () => { + const mockExecutionContext = createMockExecutionContext(); + + mockReflector.getAllAndOverride.mockReturnValue(undefined); + + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(true); + }); + + it('should return true if the apiKey type is allowed', async () => { + const mockExecutionContext = createMockExecutionContext({ + apiKey: { type: ENUM_API_KEY_TYPE.SYSTEM }, + } as any); + + mockReflector.getAllAndOverride.mockReturnValue([ + ENUM_API_KEY_TYPE.SYSTEM, + ]); + + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(true); + }); + + it('should throw BadRequestException if the apiKey type is not allowed', async () => { + const mockExecutionContext = createMockExecutionContext({ + apiKey: { type: ENUM_API_KEY_TYPE.SYSTEM }, + } as any); + + mockReflector.getAllAndOverride.mockReturnValue([ + ENUM_API_KEY_TYPE.DEFAULT, + ]); + + try { + await guard.canActivate(mockExecutionContext); + } catch (error) { + expect(error).toBeInstanceOf(BadRequestException); + expect(error.response).toEqual({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_FORBIDDEN, + message: 'apiKey.error.xApiKey.forbidden', + }); + } + }); + }); +}); diff --git a/test/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.spec.ts b/test/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.spec.ts new file mode 100644 index 000000000..e2c3a026a --- /dev/null +++ b/test/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy.spec.ts @@ -0,0 +1,257 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApiKeyXApiKeyStrategy } from 'src/modules/api-key/guards/x-api-key/strategies/api-key.x-api-key.strategy'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; + +describe('ApiKeyXApiKeyStrategy', () => { + let strategy: ApiKeyXApiKeyStrategy; + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApiKeyXApiKeyStrategy, + { + provide: ApiKeyService, + useValue: { + findOneByActiveKey: jest.fn(), + createHashApiKey: jest.fn(), + validateHashApiKey: jest.fn(), + }, + }, + { + provide: HelperDateService, + useValue: { + create: jest.fn(), + }, + }, + ], + }).compile(); + + strategy = module.get(ApiKeyXApiKeyStrategy); + apiKeyService = module.get(ApiKeyService); + helperDateService = module.get(HelperDateService); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + }); + + it('should invoke validate through callback', async () => { + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + const callback = ( + error: Error, + verified: ( + error: Error, + user?: Record, + info?: string | number + ) => Promise, + req: any + ) => { + return strategy.validate('test-key:test-secret', verified, req); + }; + + jest.spyOn(strategy, 'validate').mockImplementation(); + + strategy.verify('test-key:test-secret', callback, mockRequest); + expect(strategy.validate).toHaveBeenCalled(); + }); + + it('should call validate and pass if everything is valid', async () => { + const mockApiKeyEntity = { + _id: '1', + key: 'test-key', + hash: 'test-hash', + type: 'test-type', + isActive: true, + startDate: new Date('2020-01-01'), + endDate: new Date('2030-01-01'), + }; + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue( + mockApiKeyEntity as any + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + new Date('2024-01-01') + ); + jest.spyOn(apiKeyService, 'createHashApiKey').mockResolvedValue( + 'hashed-key' + ); + jest.spyOn(apiKeyService, 'validateHashApiKey').mockResolvedValue(true); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(apiKeyService.findOneByActiveKey).toHaveBeenCalledWith( + 'test-key' + ); + expect(helperDateService.create).toHaveBeenCalled(); + expect(apiKeyService.createHashApiKey).toHaveBeenCalledWith( + 'test-key', + 'test-secret' + ); + expect(apiKeyService.validateHashApiKey).toHaveBeenCalledWith( + 'hashed-key', + 'test-hash' + ); + expect(mockRequest.apiKey).toEqual({ + _id: '1', + name: '1', + key: 'test-key', + type: 'test-type', + }); + expect(verified).toHaveBeenCalledWith(null, mockApiKeyEntity); + }); + + it('should throw UnauthorizedException if apiKey is invalid', async () => { + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + const verified = jest.fn(); + + await strategy.validate('invalid-key', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID}`), + null, + null + ); + }); + + it('should throw ForbiddenException if apiKey is not found', async () => { + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue(null); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_NOT_FOUND}`), + null, + null + ); + }); + + it('should throw ForbiddenException if apiKey is inactive', async () => { + const mockApiKeyEntity = { + _id: '1', + key: 'test-key', + hash: 'test-hash', + type: 'test-type', + isActive: false, + startDate: new Date('2020-01-01'), + endDate: new Date('2030-01-01'), + }; + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue( + mockApiKeyEntity as any + ); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE}`), + null, + null + ); + }); + + it('should throw ForbiddenException if apiKey is expired', async () => { + const mockApiKeyEntity = { + _id: '1', + key: 'test-key', + hash: 'test-hash', + type: 'test-type', + isActive: true, + startDate: new Date('2020-01-01'), + endDate: new Date('2023-01-01'), + }; + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue( + mockApiKeyEntity as any + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + new Date('2024-01-01') + ); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_EXPIRED}`), + null, + null + ); + }); + + it('should throw ForbiddenException if apiKey is start date is less than today', async () => { + const mockApiKeyEntity = { + _id: '1', + key: 'test-key', + hash: 'test-hash', + type: 'test-type', + isActive: true, + startDate: new Date('2020-01-01'), + endDate: new Date('2025-01-01'), + }; + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue( + mockApiKeyEntity as any + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + new Date('2024-01-01') + ); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INACTIVE}`), + null, + null + ); + }); + + it('should throw UnauthorizedException if apiKey hash validation fails', async () => { + const mockApiKeyEntity = { + _id: '1', + key: 'test-key', + hash: 'test-hash', + type: 'test-type', + isActive: true, + startDate: new Date('2020-01-01'), + endDate: new Date('2030-01-01'), + }; + const mockRequest: IRequestApp = { apiKey: null } as IRequestApp; + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockResolvedValue( + mockApiKeyEntity as any + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + new Date('2024-01-01') + ); + jest.spyOn(apiKeyService, 'createHashApiKey').mockResolvedValue( + 'hashed-key' + ); + jest.spyOn(apiKeyService, 'validateHashApiKey').mockResolvedValue( + false + ); + + const verified = jest.fn(); + await strategy.validate('test-key:test-secret', verified, mockRequest); + + expect(verified).toHaveBeenCalledWith( + new Error(`${ENUM_API_KEY_STATUS_CODE_ERROR.X_API_KEY_INVALID}`), + null, + null + ); + }); +}); diff --git a/test/modules/api-key/pipes/api-key.expired.pipe.spec.ts b/test/modules/api-key/pipes/api-key.expired.pipe.spec.ts new file mode 100644 index 000000000..57dba08de --- /dev/null +++ b/test/modules/api-key/pipes/api-key.expired.pipe.spec.ts @@ -0,0 +1,62 @@ +import { BadRequestException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ApiKeyNotExpiredPipe } from 'src/modules/api-key/pipes/api-key.expired.pipe'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; + +const mockHelperDateService = { + create: jest.fn(), +}; + +describe('ApiKeyNotExpiredPipe', () => { + let pipe: ApiKeyNotExpiredPipe; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + ApiKeyNotExpiredPipe, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + ], + }).compile(); + + pipe = moduleRef.get(ApiKeyNotExpiredPipe); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + describe('transform', () => { + it('should be success transform', async () => { + expect(await pipe.transform({} as ApiKeyDoc)).toEqual({}); + }); + + it('should be throw error', async () => { + const today = new Date('2024-07-12'); + const value = { + startDate: new Date('1900-01-01'), + endDate: new Date('1900-01-01'), + }; + + mockHelperDateService.create.mockReturnValue(today); + + try { + await pipe.transform(value as ApiKeyDoc); + } catch (error) { + expect(error).toBeInstanceOf(BadRequestException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.EXPIRED, + message: 'apiKey.error.expired', + }); + } + }); + }); +}); diff --git a/test/modules/api-key/pipes/api-key.is-active.pipe.spec.ts b/test/modules/api-key/pipes/api-key.is-active.pipe.spec.ts new file mode 100644 index 000000000..e90ac54d5 --- /dev/null +++ b/test/modules/api-key/pipes/api-key.is-active.pipe.spec.ts @@ -0,0 +1,36 @@ +import { BadRequestException } from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyIsActivePipe } from 'src/modules/api-key/pipes/api-key.is-active.pipe'; +import { ApiKeyDoc } from 'src/modules/api-key/repository/entities/api-key.entity'; + +describe('ApiKeyIsActivePipe', () => { + let pipe: ApiKeyIsActivePipe; + + beforeEach(async () => { + pipe = new ApiKeyIsActivePipe([true, false]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return the value if isActive is valid', async () => { + const value = { isActive: true } as ApiKeyDoc; + + await expect(pipe.transform(value)).resolves.toEqual(value); + }); + + it('should throw BadRequestException if isActive is invalid', async () => { + const value = { isActive: null } as ApiKeyDoc; + + try { + await pipe.transform(value); + } catch (error) { + expect(error).toBeInstanceOf(BadRequestException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.IS_ACTIVE, + message: 'apiKey.error.isActiveInvalid', + }); + } + }); +}); diff --git a/test/modules/api-key/pipes/api-key.parse.pipe.spec.ts b/test/modules/api-key/pipes/api-key.parse.pipe.spec.ts new file mode 100644 index 000000000..cf8533416 --- /dev/null +++ b/test/modules/api-key/pipes/api-key.parse.pipe.spec.ts @@ -0,0 +1,59 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/modules/api-key/enums/api-key.status-code.enum'; +import { ApiKeyParsePipe } from 'src/modules/api-key/pipes/api-key.parse.pipe'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; + +const mockApiKeyService = { + findOneById: jest.fn(), +}; + +describe('ApiKeyParsePipe', () => { + let pipe: ApiKeyParsePipe; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + ApiKeyParsePipe, + { + provide: ApiKeyService, + useValue: mockApiKeyService, + }, + ], + }).compile(); + + pipe = moduleRef.get(ApiKeyParsePipe); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + describe('transform', () => { + it('should transform', async () => { + const result: any = {}; + + mockApiKeyService.findOneById.mockReturnValue(result); + + expect(await pipe.transform({})).toBe(result); + }); + + it('should be throw error', async () => { + mockApiKeyService.findOneById.mockReturnValue(undefined); + + try { + await pipe.transform({}); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + expect(error.response).toEqual({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'apiKey.error.notFound', + }); + } + }); + }); +}); diff --git a/test/modules/api-key/services/api-key.service.spec.ts b/test/modules/api-key/services/api-key.service.spec.ts new file mode 100644 index 000000000..74ce01341 --- /dev/null +++ b/test/modules/api-key/services/api-key.service.spec.ts @@ -0,0 +1,399 @@ +import { faker } from '@faker-js/faker'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { plainToInstance } from 'class-transformer'; +import mongoose from 'mongoose'; +import { + ApiKeyCreateRawRequestDto, + ApiKeyCreateRequestDto, +} from 'src/modules/api-key/dtos/request/api-key.create.request.dto'; +import { ApiKeyGetResponseDto } from 'src/modules/api-key/dtos/response/api-key.get.response.dto'; +import { ApiKeyListResponseDto } from 'src/modules/api-key/dtos/response/api-key.list.response.dto'; +import { + ApiKeyDoc, + ApiKeyEntity, + ApiKeySchema, +} from 'src/modules/api-key/repository/entities/api-key.entity'; +import { ApiKeyRepository } from 'src/modules/api-key/repository/repositories/api-key.repository'; +import { ApiKeyService } from 'src/modules/api-key/services/api-key.service'; +import { + IDatabaseCreateOptions, + IDatabaseDeleteManyOptions, + IDatabaseFindAllOptions, + IDatabaseOptions, + IDatabaseSaveOptions, + IDatabaseUpdateManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { ENUM_API_KEY_TYPE } from 'src/modules/api-key/enums/api-key.enum'; + +const mockHelperStringService = { + random: jest.fn(), +}; + +const mockConfigService = { + get: jest.fn(), +}; + +const mockHelperHashService = { + sha256: jest.fn(), + sha256Compare: jest.fn(), +}; + +const mockHelperDateService = { + startOfDay: jest.fn(), + endOfDay: jest.fn(), + create: jest.fn(), +}; + +const mockApiKeyRepository = { + findAll: jest.fn(), + findOneById: jest.fn(), + findOne: jest.fn(), + getTotal: jest.fn(), + create: jest.fn(), + save: jest.fn(), + softDelete: jest.fn(), + deleteMany: jest.fn(), + updateMany: jest.fn(), +}; + +describe('ApiKeyService', () => { + let service: ApiKeyService; + let configService: ConfigService; + let apiKeyRepository: ApiKeyRepository; + let helperHashService: HelperHashService; + let helperStringService: HelperStringService; + + let findAllOpts: IDatabaseFindAllOptions; + let findOneOpts: IDatabaseOptions; + let createOpts: IDatabaseCreateOptions; + let saveOpts: IDatabaseSaveOptions; + let deleteManyOpts: IDatabaseDeleteManyOptions; + let updateManyOpts: IDatabaseUpdateManyOptions; + let apiKeyDoc: ApiKeyDoc; + let find: Record; + let id: string; + let key: string; + let apiKeyCreateRequestDto: ApiKeyCreateRequestDto; + let apiKeyCreateRawRequestDto: ApiKeyCreateRawRequestDto; + + const entity: ApiKeyEntity = { + _id: faker.string.uuid(), + name: faker.person.jobTitle(), + type: ENUM_API_KEY_TYPE.DEFAULT, + key: faker.string.alpha(15), + hash: faker.string.alpha(20), + isActive: true, + startDate: faker.date.past(), + endDate: faker.date.future(), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deletedAt: faker.date.recent(), + deleted: false, + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + ApiKeyService, + { + provide: HelperStringService, + useValue: mockHelperStringService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: HelperHashService, + useValue: mockHelperHashService, + }, + { + provide: HelperDateService, + useValue: mockHelperDateService, + }, + { + provide: ApiKeyRepository, + useValue: mockApiKeyRepository, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + service = moduleRef.get(ApiKeyService); + configService = moduleRef.get(ConfigService); + apiKeyRepository = moduleRef.get(ApiKeyRepository); + helperHashService = moduleRef.get(HelperHashService); + helperStringService = + moduleRef.get(HelperStringService); + helperStringService = + moduleRef.get(HelperStringService); + + (configService.get as jest.Mock).mockReturnValueOnce('get'); + + findAllOpts = { + order: {}, + paging: { + limit: 1, + offset: 0, + }, + }; + findOneOpts = {}; + createOpts = {}; + saveOpts = {}; + deleteManyOpts = {}; + updateManyOpts = {}; + apiKeyDoc = {} as ApiKeyDoc; + // apiKeyEntity = ; + find = {}; + id = 'id'; + // key = ; + apiKeyCreateRequestDto = { + name: 'name', + type: ENUM_API_KEY_TYPE.SYSTEM, + endDate: new Date(), + startDate: new Date(), + }; + apiKeyCreateRawRequestDto = { + name: 'name', + type: ENUM_API_KEY_TYPE.SYSTEM, + endDate: new Date(), + startDate: new Date(), + key: 'key', + secret: 'secret', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findAll', () => { + it('should return found records', async () => { + (apiKeyRepository.findAll as jest.Mock).mockReturnValue([]); + expect(await service.findAll(find, findAllOpts)).toEqual([]); + }); + }); + + describe('findOneById', () => { + it('should return record by id', async () => { + (apiKeyRepository.findOneById as jest.Mock).mockReturnValue({}); + expect(await service.findOneById(id, findOneOpts)).toEqual({}); + }); + }); + + describe('findOne', () => { + it('should return one record', async () => { + (apiKeyRepository.findOne as jest.Mock).mockReturnValue({}); + expect(await service.findOne(find, findOneOpts)).toEqual({}); + }); + }); + + describe('findOneByKey', () => { + it('should return one record', async () => { + (apiKeyRepository.findOne as jest.Mock).mockReturnValue({}); + expect(await service.findOneByKey(key, findOneOpts)).toEqual({}); + }); + }); + + describe('findOneByActiveKey', () => { + it('should return one active record', async () => { + (apiKeyRepository.findOne as jest.Mock).mockReturnValue({}); + expect(await service.findOneByActiveKey(key, findOneOpts)).toEqual( + {} + ); + }); + }); + + describe('getTotal', () => { + it('should return total', async () => { + (apiKeyRepository.getTotal as jest.Mock).mockReturnValue(1); + expect(await service.getTotal(find, findOneOpts)).toEqual(1); + }); + }); + + describe('create', () => { + it('should success create', async () => { + (apiKeyRepository.create as jest.Mock).mockReturnValue({}); + expect( + await service.create(apiKeyCreateRequestDto, createOpts) + ).toEqual({}); + }); + }); + + describe('createRaw', () => { + it('should success create raw', async () => { + (apiKeyRepository.create as jest.Mock).mockReturnValue({ + _id: 1, + key: 'key', + }); + expect( + await service.createRaw(apiKeyCreateRawRequestDto, createOpts) + ).toEqual({ + _id: 1, + key: 'key', + secret: 'secret', + }); + }); + }); + + describe('active', () => { + it('should set active true', async () => { + (apiKeyRepository.save as jest.Mock).mockReturnValue({}); + expect(await service.active(apiKeyDoc, saveOpts)).toEqual({}); + }); + }); + + describe('inactive', () => { + it('should set inactive', async () => { + (apiKeyRepository.save as jest.Mock).mockReturnValue({}); + expect(await service.inactive(apiKeyDoc, saveOpts)).toEqual({}); + }); + }); + + describe('update', () => { + it('should update successfully', async () => { + (apiKeyRepository.save as jest.Mock).mockReturnValue({}); + expect( + await service.update(apiKeyDoc, { name: 'name' }, saveOpts) + ).toEqual({}); + }); + }); + + describe('updateDate', () => { + it('should update Date successfully', async () => { + (apiKeyRepository.save as jest.Mock).mockReturnValue({}); + expect( + await service.updateDate( + apiKeyDoc, + { startDate: new Date(), endDate: new Date() }, + saveOpts + ) + ).toEqual({}); + }); + }); + + describe('reset', () => { + it('should reset successfully', async () => { + (apiKeyRepository.save as jest.Mock).mockReturnValue({ + _id: 'id', + key: 'key', + }); + expect(await service.reset(apiKeyDoc, saveOpts)).toEqual({ + _id: 'id', + key: 'key', + }); + }); + }); + + describe('delete', () => { + it('should soft delete', async () => { + (apiKeyRepository.softDelete as jest.Mock).mockReturnValueOnce({}); + expect(await service.delete(apiKeyDoc, saveOpts)).toEqual({}); + }); + }); + + describe('validateHashApiKey', () => { + it('should validate hash api key', async () => { + (helperHashService.sha256Compare as jest.Mock).mockReturnValueOnce( + true + ); + expect(await service.validateHashApiKey('', '')).toEqual(true); + }); + }); + + describe('createKey', () => { + it('should create key', async () => { + (helperStringService.random as jest.Mock).mockReturnValueOnce( + 'random' + ); + expect(await service.createKey()).toEqual('get_random'); + }); + }); + + describe('deleteMany', () => { + it('should delete many', async () => { + (apiKeyRepository.deleteMany as jest.Mock).mockReturnValueOnce( + true + ); + expect(await service.deleteMany(find, deleteManyOpts)).toEqual( + true + ); + }); + + it('should throw an error if there repository error', async () => { + (apiKeyRepository.deleteMany as jest.Mock).mockRejectedValue( + new Error('repository error') + ); + expect(service.deleteMany(find, deleteManyOpts)).rejects.toThrow( + new Error('repository error') + ); + }); + }); + + describe('inactiveManyByEndDate', () => { + it('should be inactive by end date', async () => { + (apiKeyRepository.updateMany as jest.Mock).mockReturnValueOnce( + true + ); + expect(await service.inactiveManyByEndDate(updateManyOpts)).toEqual( + true + ); + }); + + it('should throw an error if there repository error', async () => { + (apiKeyRepository.updateMany as jest.Mock).mockRejectedValue( + new Error('repository error') + ); + expect( + service.inactiveManyByEndDate(updateManyOpts) + ).rejects.toThrow(new Error('repository error')); + }); + }); + + describe('mapList', () => { + it('should map list docs to apikey dto', async () => { + const ApiDocTest = mongoose.model('test', ApiKeySchema); + const docsTest = [entity].map(e => new ApiDocTest(e)); + const result = await service.mapList(docsTest); + const mapped = plainToInstance(ApiKeyListResponseDto, [entity]); + + expect(result).toEqual(mapped); + }); + + it('should map list entities to apikey dto', async () => { + const result = await service.mapList([entity]); + const mapped = plainToInstance(ApiKeyListResponseDto, [entity]); + + expect(result).toEqual(mapped); + }); + }); + + describe('mapGet', () => { + it('should map get doc to apikey dto', async () => { + const ApiDocTest = mongoose.model('test', ApiKeySchema); + const docTest = new ApiDocTest(entity); + const result = await service.mapGet(docTest); + const mapped = plainToInstance(ApiKeyGetResponseDto, entity); + + expect(result).toEqual(mapped); + }); + + it('should map get entity to apikey dto', async () => { + const result = await service.mapGet(entity); + const mapped = plainToInstance(ApiKeyGetResponseDto, entity); + + expect(result).toEqual(mapped); + }); + }); +}); diff --git a/test/modules/auth/decorators/auth.jwt.decorator.spec.ts b/test/modules/auth/decorators/auth.jwt.decorator.spec.ts new file mode 100644 index 000000000..1348912f4 --- /dev/null +++ b/test/modules/auth/decorators/auth.jwt.decorator.spec.ts @@ -0,0 +1,152 @@ +import { ExecutionContext, UseGuards } from '@nestjs/common'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, + AuthJwtRefreshProtected, + AuthJwtToken, +} from 'src/modules/auth/decorators/auth.jwt.decorator'; +import { AuthJwtAccessGuard } from 'src/modules/auth/guards/jwt/auth.jwt.access.guard'; +import { AuthJwtRefreshGuard } from 'src/modules/auth/guards/jwt/auth.jwt.refresh.guard'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + UseGuards: jest.fn(), + SetMetadata: jest.fn(), +})); + +/* eslint-disable */ +function getParamDecoratorFactory(decorator: any) { + class Test { + public test(@decorator() value) {} + } + + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test'); + return args[Object.keys(args)[0]].factory; +} +/* eslint-enable */ + +describe('AuthJwt Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('AuthJwtPayload', () => { + it('Should return auth payload', () => { + const payload: AuthJwtAccessPayloadDto = { + _id: faker.string.uuid(), + email: faker.internet.email(), + } as AuthJwtAccessPayloadDto; + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: payload, + }), + }), + }); + + const decorator = getParamDecoratorFactory(AuthJwtPayload); + + const result = decorator(null, executionContext); + expect(result).toBeTruthy(); + expect(result).toEqual(payload); + }); + + it('Should return user id from payload', () => { + const payload: AuthJwtAccessPayloadDto = { + _id: faker.string.uuid(), + email: faker.internet.email(), + } as AuthJwtAccessPayloadDto; + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: payload, + }), + }), + }); + + const decorator = getParamDecoratorFactory(AuthJwtPayload); + + const result = decorator('_id', executionContext); + expect(result).toBeTruthy(); + expect(result).toEqual(payload._id); + }); + }); + + describe('AuthJwtToken', () => { + it('Should return token of jwt', () => { + const token = faker.string.alphanumeric(20); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + headers: { + authorization: `Bearer ${token}`, + }, + }), + }), + }); + + const decorator = getParamDecoratorFactory(AuthJwtToken); + + const result = decorator(null, executionContext); + expect(result).toBeTruthy(); + expect(result).toEqual(token); + }); + + it('Should return undefined', () => { + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + headers: { + authorization: `Bearer`, + }, + }), + }), + }); + + const decorator = getParamDecoratorFactory(AuthJwtToken); + + const result = decorator(null, executionContext); + expect(result).toEqual(undefined); + }); + + it('Should return undefined if authorization header is undefined', () => { + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + headers: {}, + }), + }), + }); + + const decorator = getParamDecoratorFactory(AuthJwtToken); + + const result = decorator(null, executionContext); + expect(result).toEqual(undefined); + }); + }); + + describe('AuthJwtAccessProtected', () => { + it('should apply AuthJwtAccessGuard', () => { + const result = AuthJwtAccessProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(AuthJwtAccessGuard); + }); + }); + + describe('AuthJwtRefreshProtected', () => { + it('should apply AuthJwtRefreshGuard', () => { + const result = AuthJwtRefreshProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(AuthJwtRefreshGuard); + }); + }); +}); diff --git a/test/modules/auth/decorators/auth.social.decorator.spec.ts b/test/modules/auth/decorators/auth.social.decorator.spec.ts new file mode 100644 index 000000000..df988b1f0 --- /dev/null +++ b/test/modules/auth/decorators/auth.social.decorator.spec.ts @@ -0,0 +1,29 @@ +import { UseGuards } from '@nestjs/common'; +import { + AuthSocialGoogleProtected, + AuthSocialAppleProtected, +} from 'src/modules/auth/decorators/auth.social.decorator'; +import { AuthSocialAppleGuard } from 'src/modules/auth/guards/social/auth.social.apple.guard'; +import { AuthSocialGoogleGuard } from 'src/modules/auth/guards/social/auth.social.google.guard'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + UseGuards: jest.fn(), + SetMetadata: jest.fn(), +})); + +describe('AuthSocialGuard Decorators', () => { + it('AuthSocialGoogleProtected decorator should apply AuthSocialGoogleGuard', () => { + const result = AuthSocialGoogleProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(AuthSocialGoogleGuard); + }); + + it('AuthSocialAppleProtected decorator should apply AuthSocialAppleGuard', () => { + const result = AuthSocialAppleProtected(); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(AuthSocialAppleGuard); + }); +}); diff --git a/test/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.spec.ts b/test/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.spec.ts new file mode 100644 index 000000000..17557611c --- /dev/null +++ b/test/modules/auth/dtos/jwt/auth.jwt.access-payload.dto.spec.ts @@ -0,0 +1,59 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { + AuthJwtAccessPayloadDto, + AuthJwtAccessPayloadPermissionDto, +} from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; + +describe('AuthJwtAccessPayloadDto', () => { + it('should be defined', () => { + const dto = new AuthJwtAccessPayloadDto(); + expect(dto).toBeDefined(); + }); + + it('should validate example data', async () => { + const dto = plainToInstance(AuthJwtAccessPayloadDto, { + loginDate: new Date(), + loginFrom: ENUM_AUTH_LOGIN_FROM.SOCIAL_APPLE, + _id: '12345', + email: 'test@example.com', + role: 'user', + type: ENUM_POLICY_ROLE_TYPE.USER, + permissions: [], + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); + +describe('AuthJwtAccessPayloadPermissionDto', () => { + it('should be defined', () => { + const dto = new AuthJwtAccessPayloadPermissionDto(); + expect(dto).toBeDefined(); + }); + + it('should validate all action', async () => { + const dto = plainToInstance(AuthJwtAccessPayloadPermissionDto, { + action: [ + ENUM_POLICY_ACTION.CREATE, + ENUM_POLICY_ACTION.UPDATE, + ENUM_POLICY_ACTION.DELETE, + ENUM_POLICY_ACTION.EXPORT, + ENUM_POLICY_ACTION.IMPORT, + ENUM_POLICY_ACTION.MANAGE, + ENUM_POLICY_ACTION.READ, + null, + ], + subject: ENUM_POLICY_SUBJECT.ALL, + }); + + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.spec.ts b/test/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.spec.ts new file mode 100644 index 000000000..432739fa2 --- /dev/null +++ b/test/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto.spec.ts @@ -0,0 +1,20 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; + +describe('AuthJwtRefreshPayloadDto', () => { + it('should be defined', () => { + const dto = new AuthJwtRefreshPayloadDto(); + expect(dto).toBeDefined(); + }); + + it('should validate _id, loginDate, and loginFrom properties', async () => { + const dto = plainToInstance(AuthJwtRefreshPayloadDto, { + _id: '12345', + loginDate: new Date(), + loginFrom: 'web', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/request/auth.change-password.request.dto.spec.ts b/test/modules/auth/dtos/request/auth.change-password.request.dto.spec.ts new file mode 100644 index 000000000..403749db4 --- /dev/null +++ b/test/modules/auth/dtos/request/auth.change-password.request.dto.spec.ts @@ -0,0 +1,27 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthChangePasswordRequestDto } from 'src/modules/auth/dtos/request/auth.change-password.request.dto'; + +jest.mock( + 'src/common/request/validations/request.is-password.validation', + () => ({ + IsPassword: jest.fn().mockReturnValue(jest.fn()), + }) +); + +describe('AuthChangePasswordRequestDto', () => { + it('should be defined', () => { + const dto = new AuthChangePasswordRequestDto(); + expect(dto).toBeDefined(); + }); + + it('should validate example data', async () => { + const dto = plainToInstance(AuthChangePasswordRequestDto, { + newPassword: faker.string.alphanumeric(10), + oldPassword: faker.string.alphanumeric(10), + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/request/auth.login.request.dto.spec.ts b/test/modules/auth/dtos/request/auth.login.request.dto.spec.ts new file mode 100644 index 000000000..28b8b1e24 --- /dev/null +++ b/test/modules/auth/dtos/request/auth.login.request.dto.spec.ts @@ -0,0 +1,20 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthLoginRequestDto } from 'src/modules/auth/dtos/request/auth.login.request.dto'; + +describe('AuthLoginRequestDto', () => { + it('should be defined', () => { + const dto = new AuthLoginRequestDto(); + expect(dto).toBeDefined(); + }); + + it('should validate example data', async () => { + const dto = plainToInstance(AuthLoginRequestDto, { + email: faker.internet.email(), + password: faker.string.alphanumeric(), + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/request/auth.sign-up.request.dto.spec.ts b/test/modules/auth/dtos/request/auth.sign-up.request.dto.spec.ts new file mode 100644 index 000000000..06923d920 --- /dev/null +++ b/test/modules/auth/dtos/request/auth.sign-up.request.dto.spec.ts @@ -0,0 +1,28 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthSignUpRequestDto } from 'src/modules/auth/dtos/request/auth.sign-up.request.dto'; +jest.mock( + 'src/common/request/validations/request.is-password.validation', + () => ({ + IsPassword: jest.fn().mockReturnValue(jest.fn()), + }) +); + +describe('AuthSignUpRequestDto', () => { + it('should be defined', () => { + const dto = new AuthSignUpRequestDto(); + expect(dto).toBeDefined(); + }); + + it('should validate example data', async () => { + const dto = plainToInstance(AuthSignUpRequestDto, { + password: faker.string.alphanumeric(10), + email: faker.internet.email(), + name: faker.person.fullName(), + country: faker.string.uuid(), + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/response/auth.login.response.dto.spec.ts b/test/modules/auth/dtos/response/auth.login.response.dto.spec.ts new file mode 100644 index 000000000..c6d45037a --- /dev/null +++ b/test/modules/auth/dtos/response/auth.login.response.dto.spec.ts @@ -0,0 +1,38 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { AuthLoginResponseDto } from 'src/modules/auth/dtos/response/auth.login.response.dto'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; + +describe('AuthLoginResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const data = { + tokenType: 'Bearer', + roleType: ENUM_POLICY_ROLE_TYPE.USER, + expiresIn: 1000, + accessToken: faker.string.alphanumeric(), + refreshToken: faker.string.alphanumeric(), + }; + + const dto = plainToInstance(AuthLoginResponseDto, data); + + expect(dto).toBeInstanceOf(AuthLoginResponseDto); + }); + + it('Should be successful calls with image', () => { + const data = { + tokenType: 'Bearer', + roleType: ENUM_POLICY_ROLE_TYPE.USER, + expiresIn: 1000, + accessToken: faker.string.alphanumeric(), + refreshToken: faker.string.alphanumeric(), + }; + + const dto = plainToInstance(AuthLoginResponseDto, data); + + expect(dto instanceof AuthLoginResponseDto).toBe(true); + }); +}); diff --git a/test/modules/auth/dtos/response/auth.refresh.response.dto.spec.ts b/test/modules/auth/dtos/response/auth.refresh.response.dto.spec.ts new file mode 100644 index 000000000..8f73fcffb --- /dev/null +++ b/test/modules/auth/dtos/response/auth.refresh.response.dto.spec.ts @@ -0,0 +1,38 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { AuthRefreshResponseDto } from 'src/modules/auth/dtos/response/auth.refresh.response.dto'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; + +describe('AuthRefreshResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const data = { + tokenType: 'Bearer', + roleType: ENUM_POLICY_ROLE_TYPE.USER, + expiresIn: 1000, + accessToken: faker.string.alphanumeric(), + refreshToken: faker.string.alphanumeric(), + }; + + const dto = plainToInstance(AuthRefreshResponseDto, data); + + expect(dto).toBeInstanceOf(AuthRefreshResponseDto); + }); + + it('Should be successful calls with image', () => { + const data = { + tokenType: 'Bearer', + roleType: ENUM_POLICY_ROLE_TYPE.USER, + expiresIn: 1000, + accessToken: faker.string.alphanumeric(), + refreshToken: faker.string.alphanumeric(), + }; + + const dto = plainToInstance(AuthRefreshResponseDto, data); + + expect(dto instanceof AuthRefreshResponseDto).toBe(true); + }); +}); diff --git a/test/modules/auth/dtos/social/auth.social.apple-payload.dto.spec.ts b/test/modules/auth/dtos/social/auth.social.apple-payload.dto.spec.ts new file mode 100644 index 000000000..4031e58c1 --- /dev/null +++ b/test/modules/auth/dtos/social/auth.social.apple-payload.dto.spec.ts @@ -0,0 +1,18 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; + +describe('AuthSocialApplePayloadDto', () => { + it('should be defined', () => { + const dto = new AuthSocialApplePayloadDto(); + expect(dto).toBeDefined(); + }); + + it('should validate email property', async () => { + const dto = plainToInstance(AuthSocialApplePayloadDto, { + email: 'test@example.com', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/dtos/social/auth.social.google-payload.dto.spec.ts b/test/modules/auth/dtos/social/auth.social.google-payload.dto.spec.ts new file mode 100644 index 000000000..64e88c319 --- /dev/null +++ b/test/modules/auth/dtos/social/auth.social.google-payload.dto.spec.ts @@ -0,0 +1,18 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; + +describe('AuthSocialGooglePayloadDto', () => { + it('should be defined', () => { + const dto = new AuthSocialGooglePayloadDto(); + expect(dto).toBeDefined(); + }); + + it('should validate email property', async () => { + const dto = plainToInstance(AuthSocialGooglePayloadDto, { + email: 'test@example.com', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); +}); diff --git a/test/modules/auth/guards/jwt/auth.jwt.access.guard.spec.ts b/test/modules/auth/guards/jwt/auth.jwt.access.guard.spec.ts new file mode 100644 index 000000000..184bd50a5 --- /dev/null +++ b/test/modules/auth/guards/jwt/auth.jwt.access.guard.spec.ts @@ -0,0 +1,48 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UnauthorizedException } from '@nestjs/common'; +import { AuthJwtAccessGuard } from 'src/modules/auth/guards/jwt/auth.jwt.access.guard'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; + +describe('AuthJwtAccessGuard', () => { + let guard: AuthJwtAccessGuard; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthJwtAccessGuard], + }).compile(); + + guard = module.get(AuthJwtAccessGuard); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should return user if no error and user is present', () => { + const user = { id: 1, username: 'testuser' }; + const result = guard.handleRequest(null, user, null); + expect(result).toBe(user); + }); + + it('should throw UnauthorizedException if error is present', () => { + const err = new Error('Test Error'); + expect(() => guard.handleRequest(err, null, null)).toThrow( + new UnauthorizedException({ + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN, + message: 'auth.error.accessTokenUnauthorized', + _error: 'Test Error', + }) + ); + }); + + it('should throw UnauthorizedException if user is not present', () => { + const info = new Error('No user'); + expect(() => guard.handleRequest(null, null, info)).toThrow( + new UnauthorizedException({ + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_ACCESS_TOKEN, + message: 'auth.error.accessTokenUnauthorized', + _error: 'No user', + }) + ); + }); +}); diff --git a/test/modules/auth/guards/jwt/auth.jwt.refresh.guard.spec.ts b/test/modules/auth/guards/jwt/auth.jwt.refresh.guard.spec.ts new file mode 100644 index 000000000..78e460911 --- /dev/null +++ b/test/modules/auth/guards/jwt/auth.jwt.refresh.guard.spec.ts @@ -0,0 +1,48 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/modules/auth/enums/auth.status-code.enum'; +import { AuthJwtRefreshGuard } from 'src/modules/auth/guards/jwt/auth.jwt.refresh.guard'; + +describe('AuthJwtRefreshGuard', () => { + let guard: AuthJwtRefreshGuard; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthJwtRefreshGuard], + }).compile(); + + guard = module.get(AuthJwtRefreshGuard); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should return user if no error and user is present', () => { + const user = { id: 1, username: 'testuser' }; + const result = guard.handleRequest(null, user, null); + expect(result).toBe(user); + }); + + it('should throw UnauthorizedException if error is present', () => { + const err = new Error('Test Error'); + expect(() => guard.handleRequest(err, null, null)).toThrow( + new UnauthorizedException({ + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN, + message: 'auth.error.refreshTokenUnauthorized', + _error: 'Test Error', + }) + ); + }); + + it('should throw UnauthorizedException if user is not present', () => { + const info = new Error('No user'); + expect(() => guard.handleRequest(null, null, info)).toThrow( + new UnauthorizedException({ + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.JWT_REFRESH_TOKEN, + message: 'auth.error.refreshTokenUnauthorized', + _error: 'No user', + }) + ); + }); +}); diff --git a/test/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.spec.ts b/test/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.spec.ts new file mode 100644 index 000000000..446842725 --- /dev/null +++ b/test/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy.spec.ts @@ -0,0 +1,75 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { AuthJwtAccessStrategy } from 'src/modules/auth/guards/jwt/strategies/auth.jwt.access.strategy'; +import { + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; + +describe('AuthJwtAccessStrategy', () => { + let strategy: AuthJwtAccessStrategy; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthJwtAccessStrategy, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + const config = { + 'auth.jwt.prefixAuthorization': 'Bearer', + 'auth.jwt.audience': 'your-audience', + 'auth.jwt.issuer': 'your-issuer', + 'auth.jwt.accessToken.secretKey': + 'your-secret-key', + }; + return config[key]; + }), + }, + }, + ], + }).compile(); + + strategy = module.get(AuthJwtAccessStrategy); + configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + }); + + it('should validate and return the payload', async () => { + const payload: AuthJwtAccessPayloadDto = { + _id: 'test-id', + type: ENUM_POLICY_ROLE_TYPE.ADMIN, + role: 'role-id', + email: 'test@example.com', + permissions: [ + { action: 'action', subject: ENUM_POLICY_SUBJECT.ALL }, + ], + loginDate: new Date(), + loginFrom: ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE, + }; + const result = await strategy.validate(payload); + expect(result).toEqual(payload); + }); + + it('should get correct config values', () => { + expect(configService.get('auth.jwt.prefixAuthorization')).toBe( + 'Bearer' + ); + expect(configService.get('auth.jwt.audience')).toBe( + 'your-audience' + ); + expect(configService.get('auth.jwt.issuer')).toBe( + 'your-issuer' + ); + expect( + configService.get('auth.jwt.accessToken.secretKey') + ).toBe('your-secret-key'); + }); +}); diff --git a/test/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.spec.ts b/test/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.spec.ts new file mode 100644 index 000000000..75d8d2e86 --- /dev/null +++ b/test/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy.spec.ts @@ -0,0 +1,65 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { AuthJwtRefreshPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.refresh-payload.dto'; +import { AuthJwtRefreshStrategy } from 'src/modules/auth/guards/jwt/strategies/auth.jwt.refresh.strategy'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; + +describe('AuthJwtRefreshStrategy', () => { + let strategy: AuthJwtRefreshStrategy; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthJwtRefreshStrategy, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + const config = { + 'auth.jwt.prefixAuthorization': 'Bearer', + 'auth.jwt.audience': 'your-audience', + 'auth.jwt.issuer': 'your-issuer', + 'auth.jwt.refreshToken.secretKey': + 'your-secret-key', + }; + return config[key]; + }), + }, + }, + ], + }).compile(); + + strategy = module.get(AuthJwtRefreshStrategy); + configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + }); + + it('should validate and return the payload', async () => { + const payload: AuthJwtRefreshPayloadDto = { + _id: 'test-id', + loginFrom: ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE, + loginDate: new Date(), + }; + const result = await strategy.validate(payload); + expect(result).toEqual(payload); + }); + + it('should get correct config values', () => { + expect(configService.get('auth.jwt.prefixAuthorization')).toBe( + 'Bearer' + ); + expect(configService.get('auth.jwt.audience')).toBe( + 'your-audience' + ); + expect(configService.get('auth.jwt.issuer')).toBe( + 'your-issuer' + ); + expect( + configService.get('auth.jwt.refreshToken.secretKey') + ).toBe('your-secret-key'); + }); +}); diff --git a/test/modules/auth/guards/social/auth.social.apple.guard.spec.ts b/test/modules/auth/guards/social/auth.social.apple.guard.spec.ts new file mode 100644 index 000000000..2d242176e --- /dev/null +++ b/test/modules/auth/guards/social/auth.social.apple.guard.spec.ts @@ -0,0 +1,121 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { AuthSocialApplePayloadDto } from 'src/modules/auth/dtos/social/auth.social.apple-payload.dto'; +import { AuthSocialAppleGuard } from 'src/modules/auth/guards/social/auth.social.apple.guard'; + +@Injectable() +class MockAuthService { + appleGetTokenInfo = jest.fn(); +} + +describe('AuthSocialAppleGuard', () => { + let guard: AuthSocialAppleGuard; + let authService: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthSocialAppleGuard, + { provide: AuthService, useClass: MockAuthService }, + ], + }).compile(); + + guard = module.get(AuthSocialAppleGuard); + authService = module.get(AuthService); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + describe('canActivate', () => { + it('should return true for valid token', async () => { + const mockPayload: AuthSocialApplePayloadDto = { + email: 'test@example.com', + }; + (authService.appleGetTokenInfo as jest.Mock).mockResolvedValue( + mockPayload + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'Bearer validToken', + }, + user: {}, + }), + }), + } as ExecutionContext; + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(authService.appleGetTokenInfo).toHaveBeenCalledWith( + 'validToken' + ); + }); + + it('should return true for empty headers', async () => { + const mockPayload: AuthSocialApplePayloadDto = { + email: 'test@example.com', + }; + (authService.appleGetTokenInfo as jest.Mock).mockResolvedValue( + mockPayload + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: {}, + user: {}, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + + it('should throw UnauthorizedException for invalid token format', async () => { + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'InvalidTokenFormat', + }, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + + it('should throw UnauthorizedException for invalid token', async () => { + (authService.appleGetTokenInfo as jest.Mock).mockRejectedValue( + new Error('Invalid token') + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'Bearer invalidToken', + }, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + }); +}); diff --git a/test/modules/auth/guards/social/auth.social.google.guard.spec.ts b/test/modules/auth/guards/social/auth.social.google.guard.spec.ts new file mode 100644 index 000000000..f114f4a1d --- /dev/null +++ b/test/modules/auth/guards/social/auth.social.google.guard.spec.ts @@ -0,0 +1,147 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { AuthSocialGooglePayloadDto } from 'src/modules/auth/dtos/social/auth.social.google-payload.dto'; +import { AuthSocialGoogleGuard } from 'src/modules/auth/guards/social/auth.social.google.guard'; + +@Injectable() +class MockAuthService { + googleGetTokenInfo = jest.fn(); +} + +describe('AuthSocialGoogleGuard', () => { + let guard: AuthSocialGoogleGuard; + let authService: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthSocialGoogleGuard, + { provide: AuthService, useClass: MockAuthService }, + ], + }).compile(); + + guard = module.get(AuthSocialGoogleGuard); + authService = module.get(AuthService); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + describe('canActive', () => { + it('should return true for valid token', async () => { + const mockPayload: AuthSocialGooglePayloadDto = { + email: 'test@example.com', + }; + (authService.googleGetTokenInfo as jest.Mock).mockResolvedValue( + mockPayload + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'Bearer validToken', + }, + user: {}, + }), + }), + } as ExecutionContext; + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(authService.googleGetTokenInfo).toHaveBeenCalledWith( + 'validToken' + ); + }); + + it('should return true for empty headers', async () => { + const mockPayload: AuthSocialGooglePayloadDto = { + email: 'test@example.com', + }; + (authService.googleGetTokenInfo as jest.Mock).mockResolvedValue( + mockPayload + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: {}, + user: {}, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + + it('should return true for valid token', async () => { + const mockPayload: AuthSocialGooglePayloadDto = { + email: 'test@example.com', + }; + (authService.googleGetTokenInfo as jest.Mock).mockResolvedValue( + mockPayload + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'Bearer validToken', + }, + user: {}, + }), + }), + } as ExecutionContext; + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(authService.googleGetTokenInfo).toHaveBeenCalledWith( + 'validToken' + ); + }); + + it('should throw UnauthorizedException for invalid token format', async () => { + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'InvalidTokenFormat', + }, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + + it('should throw UnauthorizedException for invalid token', async () => { + (authService.googleGetTokenInfo as jest.Mock).mockRejectedValue( + new Error('Invalid token') + ); + + const context = { + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'Bearer invalidToken', + }, + }), + }), + } as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException + ); + }); + }); +}); diff --git a/test/modules/auth/services/auth.service.spec.ts b/test/modules/auth/services/auth.service.spec.ts new file mode 100644 index 000000000..1aa113d15 --- /dev/null +++ b/test/modules/auth/services/auth.service.spec.ts @@ -0,0 +1,512 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { plainToInstance } from 'class-transformer'; +import { AuthJwtAccessPayloadDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import verifyAppleToken from 'verify-apple-id-token'; +import { OAuth2Client } from 'google-auth-library'; +import { ENUM_AUTH_LOGIN_FROM } from 'src/modules/auth/enums/auth.enum'; + +jest.mock('verify-apple-id-token'); + +describe('AuthService', () => { + let service: AuthService; + let helperHashService: HelperHashService; + let helperDateService: HelperDateService; + let helperStringService: HelperStringService; + let helperEncryptionService: HelperEncryptionService; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: HelperHashService, + useValue: { + bcryptCompare: jest.fn(), + randomSalt: jest.fn(), + bcrypt: jest.fn(), + }, + }, + { + provide: HelperDateService, + useValue: { + create: jest.fn(), + forwardInSeconds: jest.fn(), + }, + }, + { + provide: HelperStringService, + useValue: { + random: jest.fn(), + }, + }, + { + provide: HelperEncryptionService, + useValue: { + jwtEncrypt: jest.fn(), + jwtVerify: jest.fn(), + jwtDecrypt: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string) => { + const config = { + 'auth.jwt.accessToken.secretKey': 'secretKey', + 'auth.jwt.accessToken.expirationTime': 3600, + 'auth.jwt.refreshToken.secretKey': + 'refreshSecretKey', + 'auth.jwt.refreshToken.expirationTime': 7200, + 'auth.jwt.prefixAuthorization': 'Bearer', + 'auth.jwt.subject': 'subject', + 'auth.jwt.audience': 'audience', + 'auth.jwt.issuer': 'issuer', + 'auth.password.expiredIn': 3600, + 'auth.password.expiredInTemporary': 1800, + 'auth.password.saltLength': 16, + 'auth.password.attempt': true, + 'auth.password.maxAttempt': 5, + 'auth.apple.clientId': 'appleClientId', + 'auth.apple.signInClientId': + 'appleSignInClientId', + 'auth.google.clientId': 'googleClientId', + 'auth.google.clientSecret': + 'googleClientSecret', + }; + return config[key]; + }), + }, + }, + { + provide: OAuth2Client, + useValue: { + verifyIdToken: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(AuthService); + helperHashService = module.get(HelperHashService); + helperDateService = module.get(HelperDateService); + helperStringService = + module.get(HelperStringService); + helperEncryptionService = module.get( + HelperEncryptionService + ); + configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createAccessToken', () => { + it('should create an access token', async () => { + const payload: any = { + _id: '123', + }; + const token: string = 'some.jwt.token'; + + jest.spyOn(helperEncryptionService, 'jwtEncrypt').mockReturnValue( + token + ); + + const result = await service.createAccessToken('subject', payload); + expect(result).toBe(token); + expect(helperEncryptionService.jwtEncrypt).toHaveBeenCalledWith( + { ...payload }, + { + secretKey: 'secretKey', + expiredIn: 3600, + audience: 'audience', + issuer: 'issuer', + subject: 'subject', + } + ); + }); + }); + + describe('validateAccessToken', () => { + it('should validate an access token', async () => { + const payload: any = { + _id: '123', + }; + + jest.spyOn(helperEncryptionService, 'jwtVerify').mockReturnValue( + true + ); + + const result = await service.validateAccessToken( + 'subject', + payload + ); + expect(result).toBe(true); + expect(helperEncryptionService.jwtVerify).toHaveBeenCalledWith( + { ...payload }, + { + secretKey: 'secretKey', + audience: 'audience', + issuer: 'issuer', + subject: 'subject', + } + ); + }); + }); + + describe('payloadAccessToken', () => { + it('should decrypt an access token', async () => { + jest.spyOn(helperEncryptionService, 'jwtDecrypt').mockReturnValue( + {} + ); + + const result = await service.payloadAccessToken('token'); + expect(result).toEqual({}); + expect(helperEncryptionService.jwtDecrypt).toHaveBeenCalledWith( + 'token' + ); + }); + }); + + describe('createRefreshToken', () => { + it('should create a new access token', async () => { + const payload: any = { + _id: '123', + }; + const token: string = 'some.jwt.token'; + + jest.spyOn(helperEncryptionService, 'jwtEncrypt').mockReturnValue( + token + ); + + const result = await service.createRefreshToken('subject', payload); + expect(result).toBe(token); + expect(helperEncryptionService.jwtEncrypt).toHaveBeenCalledWith( + { ...payload }, + { + secretKey: 'refreshSecretKey', + expiredIn: 7200, + audience: 'audience', + issuer: 'issuer', + subject: 'subject', + } + ); + }); + }); + + describe('validateRefreshToken', () => { + it('should validate a new access token', async () => { + const payload: any = { + _id: '123', + }; + + jest.spyOn(helperEncryptionService, 'jwtVerify').mockReturnValue( + true + ); + + const result = await service.validateRefreshToken( + 'subject', + payload + ); + expect(result).toBe(true); + expect(helperEncryptionService.jwtVerify).toHaveBeenCalledWith( + { ...payload }, + { + secretKey: 'refreshSecretKey', + audience: 'audience', + issuer: 'issuer', + subject: 'subject', + } + ); + }); + }); + + describe('payloadRefreshToken', () => { + it('should decrypt a new access token', async () => { + jest.spyOn(helperEncryptionService, 'jwtDecrypt').mockReturnValue( + {} + ); + + const result = await service.payloadRefreshToken('token'); + expect(result).toEqual({}); + expect(helperEncryptionService.jwtDecrypt).toHaveBeenCalledWith( + 'token' + ); + }); + }); + + describe('validateUser', () => { + it('should validate user password', async () => { + const passwordString = 'password'; + const passwordHash = 'hashedPassword'; + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValue( + true + ); + + const result = await service.validateUser( + passwordString, + passwordHash + ); + expect(result).toBe(true); + expect(helperHashService.bcryptCompare).toHaveBeenCalledWith( + passwordString, + passwordHash + ); + }); + }); + + describe('createPayloadAccessToken', () => { + it('should create payload access token', async () => { + const data = { + _id: 'userId', + role: { type: 'user', _id: 'roleId', permissions: [] }, + email: 'test@example.com', + toObject: jest.fn().mockReturnValue({ + _id: 'userId', + role: { type: 'user', _id: 'roleId', permissions: [] }, + email: 'test@example.com', + }), + }; + const loginFrom = ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE; + const loginDate = new Date(); + jest.spyOn(helperDateService, 'create').mockReturnValue(loginDate); + + const result = await service.createPayloadAccessToken( + data as any, + loginFrom + ); + expect(result).toEqual( + plainToInstance(AuthJwtAccessPayloadDto, { + _id: 'userId', + type: 'user', + role: 'roleId', + email: 'test@example.com', + permissions: [], + loginDate, + loginFrom, + }) + ); + expect(helperDateService.create).toHaveBeenCalled(); + }); + }); + + describe('createPayloadRefreshToken', () => { + it('should create payload refresh token', async () => { + const payload = { + _id: 'userId', + loginFrom: ENUM_AUTH_LOGIN_FROM.SOCIAL_GOOGLE, + loginDate: new Date(), + }; + + const result = await service.createPayloadRefreshToken( + payload as any + ); + expect(result).toEqual(payload); + }); + }); + + describe('createSalt', () => { + it('should create a salt', async () => { + const length = 16; + const salt = 'randomSalt'; + jest.spyOn(helperHashService, 'randomSalt').mockReturnValue( + salt as any + ); + + const result = await service.createSalt(length); + expect(result).toBe(salt); + expect(helperHashService.randomSalt).toHaveBeenCalledWith(length); + }); + }); + + describe('createPassword', () => { + it('should create a password', async () => { + const password = 'password'; + const salt = 'randomSalt'; + const passwordHash = 'hashedPassword'; + const passwordExpired = new Date(); + const passwordCreated = new Date(); + jest.spyOn(service, 'createSalt').mockResolvedValue(salt); + jest.spyOn(helperDateService, 'forwardInSeconds').mockReturnValue( + passwordExpired + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + passwordCreated + ); + jest.spyOn(helperHashService, 'bcrypt').mockReturnValue( + passwordHash + ); + + const result = await service.createPassword(password); + expect(result).toEqual({ + passwordHash, + passwordExpired, + passwordCreated, + salt, + }); + expect(service.createSalt).toHaveBeenCalledWith(16); + expect(helperDateService.forwardInSeconds).toHaveBeenCalledWith( + 3600 + ); + expect(helperDateService.create).toHaveBeenCalled(); + expect(helperHashService.bcrypt).toHaveBeenCalledWith( + password, + salt + ); + }); + + it('should create a temporary password', async () => { + const password = 'password'; + const salt = 'randomSalt'; + const passwordHash = 'hashedPassword'; + const passwordExpired = new Date(); + const passwordCreated = new Date(); + jest.spyOn(service, 'createSalt').mockResolvedValue(salt); + jest.spyOn(helperDateService, 'forwardInSeconds').mockReturnValue( + passwordExpired + ); + jest.spyOn(helperDateService, 'create').mockReturnValue( + passwordCreated + ); + jest.spyOn(helperHashService, 'bcrypt').mockReturnValue( + passwordHash + ); + + const result = await service.createPassword(password, { + temporary: true, + }); + expect(result).toEqual({ + passwordHash, + passwordExpired, + passwordCreated, + salt, + }); + expect(service.createSalt).toHaveBeenCalledWith(16); + expect(helperDateService.forwardInSeconds).toHaveBeenCalledWith( + 1800 + ); + expect(helperDateService.create).toHaveBeenCalled(); + expect(helperHashService.bcrypt).toHaveBeenCalledWith( + password, + salt + ); + }); + }); + + describe('createPasswordRandom', () => { + it('should create a random password', async () => { + const randomPassword = 'randomPassword'; + jest.spyOn(helperStringService, 'random').mockReturnValue( + randomPassword + ); + + const result = await service.createPasswordRandom(); + expect(result).toBe(randomPassword); + expect(helperStringService.random).toHaveBeenCalledWith(10); + }); + }); + + describe('checkPasswordExpired', () => { + it('should check if password is expired', async () => { + const passwordExpired = new Date(Date.now() - 1000); + const today = new Date(); + jest.spyOn(helperDateService, 'create').mockReturnValue(today); + + const result = await service.checkPasswordExpired(passwordExpired); + expect(result).toBe(false); + expect(helperDateService.create).toHaveBeenCalled(); + }); + }); + + describe('getTokenType', () => { + it('should return token type', async () => { + expect(await service.getTokenType()).toEqual('Bearer'); + }); + }); + + describe('getAccessTokenExpirationTime', () => { + it('should return token expiration time', async () => { + expect(await service.getAccessTokenExpirationTime()).toEqual( + configService.get('auth.jwt.accessToken.expirationTime') + ); + }); + }); + + describe('getRefreshTokenExpirationTime', () => { + it('should return refresh token expiration time', async () => { + expect(await service.getRefreshTokenExpirationTime()).toEqual( + configService.get('auth.jwt.refreshToken.expirationTime') + ); + }); + }); + + describe('getIssuer', () => { + it('should return issuer', async () => { + expect(await service.getIssuer()).toEqual( + configService.get('auth.jwt.issuer') + ); + }); + }); + + describe('getAudience', () => { + it('should return audience', async () => { + expect(await service.getAudience()).toEqual( + configService.get('auth.jwt.audience') + ); + }); + }); + + describe('getPasswordAttempt', () => { + it('should return password attempt', async () => { + expect(await service.getPasswordAttempt()).toEqual( + configService.get('auth.password.attempt') + ); + }); + }); + + describe('getPasswordMaxAttempt', () => { + it('should return max password attempt', async () => { + expect(await service.getPasswordMaxAttempt()).toEqual( + configService.get('auth.password.maxAttempt') + ); + }); + }); + + describe('appleGetTokenInfo', () => { + it('should return apple token info', async () => { + const result = { email: 'akan@kadence.com' }; + (verifyAppleToken as jest.Mock).mockReturnValue(result); + + expect(await service.appleGetTokenInfo('idToken')).toEqual(result); + }); + }); + + describe('googleGetTokenInfo', () => { + it('should get Google token info', async () => { + const idToken = 'idToken'; + const email = 'test@example.com'; + const mockPayload = { email }; + const mockLoginTicket = { + getPayload: jest.fn().mockReturnValue(mockPayload), + }; + + jest.spyOn( + service['googleClient'], + 'verifyIdToken' + ).mockImplementation(() => mockLoginTicket); + + const result = await service.googleGetTokenInfo(idToken); + expect(result).toEqual({ email }); + expect(service['googleClient'].verifyIdToken).toHaveBeenCalledWith({ + idToken, + }); + }); + }); +}); diff --git a/test/modules/aws/dtos/aws.s3-multipart.dto.spec.ts b/test/modules/aws/dtos/aws.s3-multipart.dto.spec.ts new file mode 100644 index 000000000..a419e09c8 --- /dev/null +++ b/test/modules/aws/dtos/aws.s3-multipart.dto.spec.ts @@ -0,0 +1,46 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { + AwsS3MultipartDto, + AwsS3MultipartPartDto, +} from 'src/modules/aws/dtos/aws.s3-multipart.dto'; + +describe('aws.s3-multipart.dto.ts', () => { + it('should create a valid AwsS3MultipartPartDto object', () => { + const mockAwsS3MultipartPartDto: AwsS3MultipartPartDto = { + eTag: faker.string.alpha(10), + partNumber: faker.number.int(10), + size: faker.number.int(10), + }; + + const dto = plainToInstance( + AwsS3MultipartPartDto, + mockAwsS3MultipartPartDto + ); + + expect(dto).toBeInstanceOf(AwsS3MultipartPartDto); + }); + + it('should create a valid AwsS3MultipartDto object', () => { + const mock: AwsS3MultipartDto = { + uploadId: faker.string.uuid(), + lastPartNumber: faker.number.int(10), + maxPartNumber: faker.number.int(10), + parts: [] as AwsS3MultipartPartDto[], + bucket: faker.string.alpha(10), + path: faker.string.alpha(15), + pathWithFilename: faker.string.alpha(20), + filename: faker.string.alpha(10), + completedUrl: faker.string.alpha(10), + baseUrl: faker.string.alpha(10), + mime: faker.string.alpha(10), + duration: faker.number.int(10), + size: faker.number.int(10), + }; + + const dto = plainToInstance(AwsS3MultipartDto, mock); + + expect(dto).toBeInstanceOf(AwsS3MultipartDto); + }); +}); diff --git a/test/modules/aws/dtos/aws.s3.dto.spec.ts b/test/modules/aws/dtos/aws.s3.dto.spec.ts new file mode 100644 index 000000000..a9015be55 --- /dev/null +++ b/test/modules/aws/dtos/aws.s3.dto.spec.ts @@ -0,0 +1,24 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { AwsS3Dto } from 'src/modules/aws/dtos/aws.s3.dto'; + +describe('aws.s3.dto.ts', () => { + it('should create a valid AwsS3Dto object', () => { + const mockAwsS3Dto: AwsS3Dto = { + bucket: faker.string.alpha(10), + path: faker.string.alpha(15), + pathWithFilename: faker.string.alpha(20), + filename: faker.string.alpha(10), + completedUrl: faker.string.alpha(10), + baseUrl: faker.string.alpha(10), + mime: faker.string.alpha(10), + duration: faker.number.int(10), + size: faker.number.int(10), + }; + + const dto = plainToInstance(AwsS3Dto, mockAwsS3Dto); + + expect(dto).toBeInstanceOf(AwsS3Dto); + }); +}); diff --git a/test/modules/aws/dtos/aws.se-presignurl.dto.spec.ts b/test/modules/aws/dtos/aws.se-presignurl.dto.spec.ts new file mode 100644 index 000000000..903905370 --- /dev/null +++ b/test/modules/aws/dtos/aws.se-presignurl.dto.spec.ts @@ -0,0 +1,25 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { AwsS3PresignUrlDto } from 'src/modules/aws/dtos/aws.s3-presign-url.dto'; + +describe('aws.s3.dto.ts', () => { + it('should create a valid AwsS3Dto object', () => { + const mockAwsS3PresignUrlDto: AwsS3PresignUrlDto = { + bucket: faker.string.alpha(10), + path: faker.string.alpha(15), + pathWithFilename: faker.string.alpha(20), + filename: faker.string.alpha(10), + completedUrl: faker.string.alpha(10), + baseUrl: faker.string.alpha(10), + mime: faker.string.alpha(10), + duration: faker.number.int({ max: 10 }), + size: faker.number.int({ max: 10 }), + expiredIn: faker.number.int({ max: 10 }), + }; + + const dto = plainToInstance(AwsS3PresignUrlDto, mockAwsS3PresignUrlDto); + + expect(dto).toBeInstanceOf(AwsS3PresignUrlDto); + }); +}); diff --git a/test/modules/aws/dtos/aws.ses.dto.spec.ts b/test/modules/aws/dtos/aws.ses.dto.spec.ts new file mode 100644 index 000000000..23ac5906c --- /dev/null +++ b/test/modules/aws/dtos/aws.ses.dto.spec.ts @@ -0,0 +1,103 @@ +import 'reflect-metadata'; +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { + AwsSESCreateTemplateDto, + AwsSESGetTemplateDto, + AwsSESSendBulkDto, + AwsSESSendBulkRecipientsDto, + AwsSESSendDto, + AwsSESUpdateTemplateDto, +} from 'src/modules/aws/dtos/aws.ses.dto'; + +describe('aws.ses.dto.ts', () => { + it('should create a valid AwsSESCreateTemplateDto object', () => { + const mockAwsSESCreateTemplateDto: AwsSESCreateTemplateDto = { + name: faker.string.alpha(10), + htmlBody: faker.string.alpha(15), + subject: faker.string.alpha(20), + plainTextBody: faker.string.alpha(10), + }; + + const dto = plainToInstance( + AwsSESCreateTemplateDto, + mockAwsSESCreateTemplateDto + ); + + expect(dto).toBeInstanceOf(AwsSESCreateTemplateDto); + }); + + it('should create a valid AwsSESUpdateTemplateDto object', () => { + const mockAwsSESUpdateTemplateDto: AwsSESCreateTemplateDto = { + name: faker.string.alpha(10), + htmlBody: faker.string.alpha(15), + subject: faker.string.alpha(20), + plainTextBody: faker.string.alpha(10), + }; + + const dto = plainToInstance( + AwsSESUpdateTemplateDto, + mockAwsSESUpdateTemplateDto + ); + + expect(dto).toBeInstanceOf(AwsSESCreateTemplateDto); + }); + + it('should create a valid AwsSESGetTemplateDto object', () => { + const mockAwsSESGetTemplateDto: AwsSESGetTemplateDto = { + name: faker.string.alpha(10), + }; + + const dto = plainToInstance( + AwsSESGetTemplateDto, + mockAwsSESGetTemplateDto + ); + + expect(dto).toBeInstanceOf(AwsSESGetTemplateDto); + }); + + it('should create a valid AwsSESSendDto object', () => { + const mockAwsSESSendDto = { + templateName: faker.string.alpha(10), + templateData: {}, + sender: faker.string.alpha(10), + replyTo: faker.string.alpha(10), + recipients: [faker.string.alpha(10)], + cc: [faker.string.alpha(10)], + bcc: [faker.string.alpha(10)], + }; + + const dto = plainToInstance(AwsSESSendDto, mockAwsSESSendDto); + + expect(dto).toBeInstanceOf(AwsSESSendDto); + }); + + it('should create a valid AwsSESSendBulkRecipientsDto object', () => { + const mockAwsSESSendBulkRecipientsDto = { + templateData: {}, + recipient: faker.string.alpha(10), + }; + + const dto = plainToInstance( + AwsSESSendBulkRecipientsDto, + mockAwsSESSendBulkRecipientsDto + ); + + expect(dto).toBeInstanceOf(AwsSESSendBulkRecipientsDto); + }); + + it('should create a valid AwsSESSendBulkDto object', () => { + const mockAwsSESSendBulkDto = { + templateName: faker.string.alpha(10), + sender: faker.string.alpha(10), + replyTo: faker.string.alpha(10), + cc: [faker.string.alpha(10)], + bcc: [faker.string.alpha(10)], + recipients: { recipient: faker.string.alpha(10) }, + }; + + const dto = plainToInstance(AwsSESSendBulkDto, mockAwsSESSendBulkDto); + + expect(dto).toBeInstanceOf(AwsSESSendBulkDto); + }); +}); diff --git a/test/modules/aws/services/aws.s3.service.spec.ts b/test/modules/aws/services/aws.s3.service.spec.ts new file mode 100644 index 000000000..122159b3a --- /dev/null +++ b/test/modules/aws/services/aws.s3.service.spec.ts @@ -0,0 +1,703 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { + S3Client, + ListBucketsCommandOutput, + ListObjectsV2CommandOutput, + PutObjectCommandOutput, + ObjectCannedACL, +} from '@aws-sdk/client-s3'; +import { AwsS3Service } from 'src/modules/aws/services/aws.s3.service'; +import { AWS_S3_MAX_PART_NUMBER } from 'src/modules/aws/constants/aws.constant'; +import { + IAwsS3PutPresignUrlFile, + IAwsS3PutPresignUrlOptions, +} from 'src/modules/aws/interfaces/aws.interface'; +import presign from '@aws-sdk/s3-request-presigner'; + +jest.mock('@aws-sdk/s3-request-presigner', () => ({ + getSignedUrl: jest.fn().mockReturnValue('https://example.com/aws-presign'), +})); + +const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'aws.s3.credential.key': + return 'mockAccessKeyId'; + case 'aws.s3.credential.secret': + return 'mockSecretAccessKey'; + case 'aws.s3.region': + return 'mockRegion'; + case 'aws.s3.bucket': + return 'mockBucket'; + case 'aws.s3.baseUrl': + return 'mockBaseUrl'; + default: + return 10000; + } + }), +}; + +const mockS3Client = { + send: jest.fn(), +}; + +describe('AwsS3Service', () => { + let service: AwsS3Service; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AwsS3Service, + { provide: ConfigService, useValue: mockConfigService }, + { provide: S3Client, useValue: mockS3Client }, + ], + }).compile(); + + service = module.get(AwsS3Service); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('checkBucketExistence', () => { + it('should return the bucket existence details', async () => { + const mockOutput = { $metadata: { httpStatusCode: 200 } }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const result = await service.checkBucketExistence(); + expect(result).toEqual(mockOutput); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect(service.checkBucketExistence()).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('listBucket', () => { + it('should return a list of bucket names', async () => { + const mockOutput: ListBucketsCommandOutput = { + Buckets: [{ Name: 'mockBucket' }], + $metadata: { httpStatusCode: 200 }, + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const result = await service.listBucket(); + expect(result).toEqual(['mockBucket']); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect(service.listBucket()).rejects.toThrow('Mock Error'); + }); + }); + + describe('listItemInBucket', () => { + it('should return a list of items in the bucket', async () => { + const mockOutput: ListObjectsV2CommandOutput = { + Contents: [{ Key: '/mock/path/file.txt', Size: 1234 }], + $metadata: { httpStatusCode: 200 }, + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const result = await service.listItemInBucket('mock/path'); + expect(result).toEqual([ + { + bucket: 'mockBucket', + path: '/mock/path', + pathWithFilename: '/mock/path/file.txt', + filename: 'file.txt', + completedUrl: 'mockBaseUrl/mock/path/file.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 1234, + }, + ]); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect(service.listItemInBucket('mock/path')).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('getItemInBucket', () => { + it('should return the item from the bucket', async () => { + const mockOutput = { + Body: 'mockBody', + $metadata: { httpStatusCode: 200 }, + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const result = await service.getItemInBucket('mock/path/file.txt'); + expect(result).toEqual('mockBody'); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect( + service.getItemInBucket('mock/path/file.txt') + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('putItemInBucket', () => { + it('should upload an item to the bucket', async () => { + const mockOutput: PutObjectCommandOutput = { + $metadata: { httpStatusCode: 200 }, + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { + path: '/mock/path', + customFilename: 'customFilename', + }; + + const result = await service.putItemInBucket(file, options); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/mock/path', + pathWithFilename: '/mock/path/customFilename.txt', + filename: 'customFilename.txt', + completedUrl: 'mockBaseUrl/mock/path/customFilename.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 1024, + }); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + + await expect(service.putItemInBucket(file)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('putItemInBucketWithAcl', () => { + it('should upload an item to the bucket with acl', async () => { + const mockOutput: PutObjectCommandOutput = { + $metadata: { httpStatusCode: 200 }, + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { + path: '/mock/path', + acl: ObjectCannedACL.public_read, + customFilename: 'customFilename', + }; + + const result = await service.putItemInBucketWithAcl(file, options); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/mock/path', + pathWithFilename: '/mock/path/customFilename.txt', + filename: 'customFilename.txt', + completedUrl: 'mockBaseUrl/mock/path/customFilename.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 1024, + }); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + + await expect(service.putItemInBucketWithAcl(file)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('deleteItemInBucket', () => { + it('should delete an item in bucket', async () => { + jest.spyOn(service['s3Client'], 'send').mockReturnThis(); + + expect( + await service.deleteItemInBucket('path/filename.txt') + ).toEqual(undefined); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect( + service.deleteItemInBucket('path/filename.txt') + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('deleteItemsInBucket', () => { + it('should delete an items in bucket', async () => { + jest.spyOn(service['s3Client'], 'send').mockReturnThis(); + + expect( + await service.deleteItemsInBucket(['path/filename.txt']) + ).toEqual(undefined); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect( + service.deleteItemsInBucket(['path/filename.txt']) + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('deleteFolder', () => { + it('should delete a folder', async () => { + const mockOutput = { + Contents: [ + { + Key: 'key', + }, + ], + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + expect(await service.deleteFolder('/dir')).toEqual(undefined); + }); + + it('should throw an error if S3Client send fails', async () => { + const mockOutput = { + Contents: [ + { + Key: 'key', + }, + ], + }; + jest.spyOn(service['s3Client'], 'send') + .mockReturnValueOnce(mockOutput as any) + .mockRejectedValue(new Error('Mock Error') as never); + + await expect(service.deleteFolder('/dir')).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('createMultiPart', () => { + it('should create multi part', async () => { + const mockOutput = { UploadId: 1 }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { + path: 'mock/path', + customFilename: 'customFilename', + }; + + const result = await service.createMultiPart(file, 1, options); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/mock/path', + pathWithFilename: '/mock/path/customFilename.txt', + filename: 'customFilename.txt', + completedUrl: 'mockBaseUrl/mock/path/customFilename.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 0, + uploadId: 1, + lastPartNumber: 0, + maxPartNumber: 1, + parts: [], + }); + }); + + it('should create multi part with no options', async () => { + const mockOutput = { UploadId: 1 }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + + const result = await service.createMultiPart(file, 1); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/', + pathWithFilename: '/file.txt', + filename: 'file.txt', + completedUrl: 'mockBaseUrl/file.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 0, + uploadId: 1, + lastPartNumber: 0, + maxPartNumber: 1, + parts: [], + }); + }); + + it('should throw an error max part number', async () => { + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { path: 'mock/path' }; + + await expect( + service.createMultiPart(file, 100001, options) + ).rejects.toThrow( + `Max part number is greater than ${AWS_S3_MAX_PART_NUMBER}` + ); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { path: 'mock/path' }; + + await expect( + service.createMultiPart(file, options as any) + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('createMultiPartWIthAcl', () => { + it('should create multi part with acl', async () => { + const mockOutput = { UploadId: 1 }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { + path: 'mock/path', + customFilename: 'customFilename', + acl: ObjectCannedACL.public_read, + }; + + const result = await service.createMultiPartWithAcl( + file, + 1, + options + ); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/mock/path', + pathWithFilename: '/mock/path/customFilename.txt', + filename: 'customFilename.txt', + completedUrl: 'mockBaseUrl/mock/path/customFilename.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 0, + uploadId: 1, + lastPartNumber: 0, + maxPartNumber: 1, + parts: [], + }); + }); + + it('should create multi part with acl no options', async () => { + const mockOutput = { UploadId: 1 }; + jest.spyOn(service['s3Client'], 'send').mockReturnValue( + mockOutput as any + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + + const result = await service.createMultiPartWithAcl(file, 1); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/', + pathWithFilename: '/file.txt', + filename: 'file.txt', + completedUrl: 'mockBaseUrl/file.txt', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 0, + uploadId: 1, + lastPartNumber: 0, + maxPartNumber: 1, + parts: [], + }); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const file = { + originalname: 'file.txt', + buffer: Buffer.from('file content'), + size: 1024, + }; + const options = { path: 'mock/path' }; + + await expect( + service.createMultiPartWithAcl(file, options as any) + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('uploadPart', () => { + it('should upload a part', async () => { + const mockOutput = { + ETag: 'etag', + }; + jest.spyOn(service['s3Client'], 'send').mockReturnValueOnce( + mockOutput as any + ); + + const multipart = { + path: 'path', + uploadId: 1, + }; + expect( + await service.uploadPart(multipart as any, 1, 'content') + ).toEqual({ + eTag: 'etag', + partNumber: 1, + size: 7, + }); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const multipart = { + path: 'path', + uploadId: 1, + }; + await expect( + service.uploadPart(multipart as any, 1, 'content') + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('updateMultiPart', () => { + it('should update multi part', async () => { + expect( + await service.updateMultiPart( + { + size: 1, + parts: [], + } as any, + { size: 1, partNumber: 1, eTag: 'etag' } as any + ) + ).toEqual({ + size: 2, + lastPartNumber: 1, + parts: [{ eTag: 'etag', size: 1, partNumber: 1 }], + }); + }); + }); + + describe('completeMultipart', () => { + it('should complete multipart', async () => { + const mockOutput = {}; + jest.spyOn(service['s3Client'], 'send').mockReturnValueOnce( + mockOutput as any + ); + expect( + await service.completeMultipart({ + path: 'path', + uploadId: 1, + parts: [], + } as any) + ); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect( + service.completeMultipart({ + path: 'path', + uploadId: 1, + parts: [], + } as any) + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('abortMultipart', () => { + it('should abort multipart', async () => { + const mockOutput = {}; + jest.spyOn(service['s3Client'], 'send').mockReturnValueOnce( + mockOutput as any + ); + expect( + await service.abortMultipart({ + path: 'path', + uploadId: 1, + } as any) + ); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(service['s3Client'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect( + service.abortMultipart({ + path: 'path', + uploadId: 1, + } as any) + ).rejects.toThrow('Mock Error'); + }); + }); + + describe('setPresignUrl', () => { + it('should return presign url', async () => { + const file: IAwsS3PutPresignUrlFile = { + filename: 'file.txt', + size: 1024, + duration: 1, + }; + + const result = await service.setPresignUrl(file); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/', + pathWithFilename: '/file.txt', + filename: 'file.txt', + completedUrl: 'https://example.com/aws-presign', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 1024, + duration: 1, + expiredIn: 10000, + }); + }); + + it('should return presign url with options', async () => { + const file: IAwsS3PutPresignUrlFile = { + filename: 'file.txt', + size: 1024, + duration: 1, + }; + + const options: IAwsS3PutPresignUrlOptions = { + path: '/path/new', + }; + + const result = await service.setPresignUrl(file, options); + expect(result).toEqual({ + bucket: 'mockBucket', + path: '/path/new', + pathWithFilename: '/path/new/file.txt', + filename: 'file.txt', + completedUrl: 'https://example.com/aws-presign', + baseUrl: 'mockBaseUrl', + mime: 'txt', + size: 1024, + duration: 1, + expiredIn: 10000, + }); + }); + + it('should throw an error if S3Client send fails', async () => { + jest.spyOn(presign, 'getSignedUrl').mockRejectedValue( + new Error('presign error') + ); + + const file: IAwsS3PutPresignUrlFile = { + filename: 'file.txt', + size: 1024, + duration: 1, + }; + + await expect(service.setPresignUrl(file)).rejects.toThrow( + new Error('presign error') + ); + }); + }); +}); diff --git a/test/modules/aws/services/aws.ses.service.spec.ts b/test/modules/aws/services/aws.ses.service.spec.ts new file mode 100644 index 000000000..f73c31c2d --- /dev/null +++ b/test/modules/aws/services/aws.ses.service.spec.ts @@ -0,0 +1,369 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { SESClient } from '@aws-sdk/client-ses'; +import { AwsSESService } from 'src/modules/aws/services/aws.ses.service'; +import { + AwsSESGetTemplateDto, + AwsSESCreateTemplateDto, +} from 'src/modules/aws/dtos/aws.ses.dto'; + +const mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'aws.ses.credential.key': + return 'mockAccessKeyId'; + case 'aws.ses.credential.secret': + return 'mockSecretAccessKey'; + case 'aws.ses.region': + return 'mockRegion'; + default: + return null; + } + }), +}; + +const mockSESClient = { + send: jest.fn(), +}; + +describe('AwsSESService', () => { + let service: AwsSESService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AwsSESService, + { provide: ConfigService, useValue: mockConfigService }, + { provide: SESClient, useValue: mockSESClient }, + ], + }).compile(); + + service = module.get(AwsSESService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('listTemplates', () => { + it('should return a list of templates', async () => { + const mockOutput = { + TemplatesMetadata: [], + NextToken: 'mockToken', + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const result = await service.listTemplates(); + expect(result).toEqual(mockOutput); + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + await expect(service.listTemplates()).rejects.toThrow('Mock Error'); + }); + }); + + describe('getTemplate', () => { + it('should return the template details', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto: AwsSESGetTemplateDto = { name: 'mockName' }; + const result = await service.getTemplate(dto); + expect(result).toEqual(mockOutput); + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto: AwsSESGetTemplateDto = { name: 'mockName' }; + await expect(service.getTemplate(dto)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('createTemplate', () => { + it('should create the template', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + htmlBody: 'htmlBody', + plainTextBody: 'plainTextBody', + }; + expect(await service.createTemplate(dto)).toEqual(mockOutput); + }); + + it('should throw error body null', async () => { + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + }; + + try { + await service.createTemplate(dto); + } catch (error) { + expect(error.message).toEqual('body is null'); + } + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + htmlBody: 'htmlBody', + plainTextBody: 'plainTextBody', + }; + await expect(service.createTemplate(dto)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('updateTemplate', () => { + it('should update the template', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + htmlBody: 'htmlBody', + plainTextBody: 'plainTextBody', + }; + expect(await service.updateTemplate(dto)).toEqual(mockOutput); + }); + + it('should throw error body null', async () => { + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + }; + + try { + await service.updateTemplate(dto); + } catch (error) { + expect(error.message).toEqual('body is null'); + } + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto: AwsSESCreateTemplateDto = { + name: 'name', + subject: 'subject', + htmlBody: 'htmlBody', + plainTextBody: 'plainTextBody', + }; + await expect(service.updateTemplate(dto)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('deleteTemplate', () => { + it('should delete the template', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto = { name: 'name' }; + expect(await service.deleteTemplate(dto)).toEqual(mockOutput); + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto = { name: 'name' }; + await expect(service.deleteTemplate(dto)).rejects.toThrow( + 'Mock Error' + ); + }); + }); + + describe('send', () => { + it('should send', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto = { + recipients: ['recipients'], + sender: 'sender', + replyTo: 'replyTo', + bcc: ['bcc'], + cc: ['cc'], + templateName: 'templateName', + templateData: {}, + }; + expect(await service.send(dto)).toEqual(mockOutput); + }); + + it('should send without replyTo bcc cc templateData', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto = { + recipients: ['recipients'], + sender: 'sender', + templateName: 'templateName', + }; + expect(await service.send(dto)).toEqual(mockOutput); + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto = { + recipients: ['recipients'], + sender: 'sender', + replyTo: 'replyTo', + bcc: ['bcc'], + cc: ['cc'], + templateName: 'templateName', + templateData: {}, + }; + await expect(service.send(dto)).rejects.toThrow('Mock Error'); + }); + }); + + describe('sendBulk', () => { + it('should send bulk', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto = { + recipients: [ + { recipients: 'recipients', templateData: {} } as any, + ], + sender: 'sender', + replyTo: 'replyTo', + bcc: ['bcc'], + cc: ['cc'], + templateName: 'templateName', + }; + expect(await service.sendBulk(dto)).toEqual(mockOutput); + }); + + it('should send bulk without bcc cc templateData replyTo', async () => { + const mockOutput = { + Template: { + TemplateName: 'mockName', + HtmlPart: 'mockHtml', + SubjectPart: 'mockSubject', + TextPart: 'mockText', + }, + }; + jest.spyOn(service['sesClient'], 'send').mockReturnValue( + mockOutput as any + ); + + const dto = { + recipients: [{ recipients: 'recipients' } as any], + sender: 'sender', + templateName: 'templateName', + }; + expect(await service.sendBulk(dto)).toEqual(mockOutput); + }); + + it('should throw an error if SESClient send fails', async () => { + jest.spyOn(service['sesClient'], 'send').mockRejectedValue( + new Error('Mock Error') as never + ); + + const dto = { + recipients: [ + { recipients: 'recipients', templateData: {} } as any, + ], + sender: 'sender', + replyTo: 'replyTo', + bcc: ['bcc'], + cc: ['cc'], + templateName: 'templateName', + }; + await expect(service.sendBulk(dto)).rejects.toThrow('Mock Error'); + }); + }); +}); diff --git a/test/modules/country/dtos/request/country.create.request.dto.spec.ts b/test/modules/country/dtos/request/country.create.request.dto.spec.ts new file mode 100644 index 000000000..390cb530f --- /dev/null +++ b/test/modules/country/dtos/request/country.create.request.dto.spec.ts @@ -0,0 +1,41 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { CountryCreateRequestDto } from 'src/modules/country/dtos/request/country.create.request.dto'; + +describe('CountryCreateRequestDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const dto = new CountryCreateRequestDto(); + dto.name = 'Indonesia'; + dto.alpha2Code = 'ID'; + dto.alpha3Code = 'IDN'; + dto.domain = 'id'; + dto.fipsCode = 'ID'; + dto.numericCode = '360'; + dto.phoneCode = ['62']; + dto.continent = 'Asia'; + dto.timeZone = 'Asia/Jakarta'; + + expect(dto).toBeInstanceOf(CountryCreateRequestDto); + }); + + it('Should throw an error when request not match with validation', async () => { + const dto = new CountryCreateRequestDto(); + dto.alpha2Code = 'ID'; + dto.alpha3Code = 'IDN'; + dto.domain = 'id'; + dto.fipsCode = 'ID'; + dto.numericCode = '360'; + dto.phoneCode = ['62']; + dto.continent = 'Asia'; + dto.timeZone = 'Asia/Jakarta'; + + const instance = plainToInstance(CountryCreateRequestDto, dto); + const errors = await validate(instance); + + expect(errors.length).toBe(1); + }); +}); diff --git a/test/modules/country/dtos/response/country.get.response.dto.spec.ts b/test/modules/country/dtos/response/country.get.response.dto.spec.ts new file mode 100644 index 000000000..011a9ad4c --- /dev/null +++ b/test/modules/country/dtos/response/country.get.response.dto.spec.ts @@ -0,0 +1,63 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { ENUM_FILE_MIME } from 'src/common/file/enums/file.enum'; +import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; + +describe('CountryGetResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const countryEntity = { + _id: faker.string.uuid(), + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + }; + + const dto = plainToInstance(CountryGetResponseDto, countryEntity); + + expect(dto).toBeInstanceOf(CountryGetResponseDto); + }); + + it('Should be successful calls with image', () => { + const countryEntity = { + _id: faker.string.uuid(), + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + image: { + baseUrl: faker.internet.url(), + bucket: faker.lorem.word(), + completedUrl: faker.internet.url(), + filename: faker.lorem.word(), + mime: ENUM_FILE_MIME.CSV, + path: faker.lorem.word(), + pathWithFilename: faker.lorem.word(), + size: 10, + duration: 5, + }, + }; + + const dto = plainToInstance(CountryGetResponseDto, countryEntity); + + expect(dto instanceof CountryGetResponseDto).toBe(true); + }); +}); diff --git a/test/modules/country/dtos/response/country.list.response.dto.spec.ts b/test/modules/country/dtos/response/country.list.response.dto.spec.ts new file mode 100644 index 000000000..58149ee6d --- /dev/null +++ b/test/modules/country/dtos/response/country.list.response.dto.spec.ts @@ -0,0 +1,36 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; + +describe('CountryListResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const countryEntity = { + _id: faker.string.uuid(), + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + }; + + const dto = plainToInstance(CountryListResponseDto, countryEntity); + + expect(dto).toBeInstanceOf(CountryListResponseDto); + expect(dto.alpha3Code).toBeUndefined(); + expect(dto.fipsCode).toBeUndefined(); + expect(dto.continent).toBeUndefined(); + expect(dto.domain).toBeUndefined(); + expect(dto.timeZone).toBeUndefined(); + expect(dto.numericCode).toBeUndefined(); + }); +}); diff --git a/test/modules/country/dtos/response/country.short.response.dto.spec.ts b/test/modules/country/dtos/response/country.short.response.dto.spec.ts new file mode 100644 index 000000000..d447289e5 --- /dev/null +++ b/test/modules/country/dtos/response/country.short.response.dto.spec.ts @@ -0,0 +1,38 @@ +import { faker } from '@faker-js/faker'; +import { plainToInstance } from 'class-transformer'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; + +describe('CountryShortResponseDto', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should be successful calls', () => { + const countryEntity = { + _id: faker.string.uuid(), + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + }; + + const dto = plainToInstance(CountryShortResponseDto, countryEntity); + + expect(dto).toBeInstanceOf(CountryShortResponseDto); + expect(dto.alpha3Code).toBeUndefined(); + expect(dto.fipsCode).toBeUndefined(); + expect(dto.continent).toBeUndefined(); + expect(dto.domain).toBeUndefined(); + expect(dto.timeZone).toBeUndefined(); + expect(dto.numericCode).toBeUndefined(); + expect(dto.createdAt).toBeUndefined(); + expect(dto.updatedAt).toBeUndefined(); + }); +}); diff --git a/test/modules/country/pipes/country.parse.pipe.spec.ts b/test/modules/country/pipes/country.parse.pipe.spec.ts new file mode 100644 index 000000000..0bc183114 --- /dev/null +++ b/test/modules/country/pipes/country.parse.pipe.spec.ts @@ -0,0 +1,62 @@ +import { faker } from '@faker-js/faker'; +import { NotFoundException } from '@nestjs/common'; +import { CountryParsePipe } from 'src/modules/country/pipes/country.parse.pipe'; +import { CountryEntity } from 'src/modules/country/repository/entities/country.entity'; + +describe('CountryParsePipe', () => { + let pipe: CountryParsePipe; + + const countryId = faker.string.uuid(); + + const country: CountryEntity = { + _id: countryId, + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + } as CountryEntity; + + const mockCountryService = { + findOneById: jest.fn(), + }; + + beforeEach(async () => { + pipe = new CountryParsePipe(mockCountryService as any); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + describe('transform', () => { + it('Should throw a NotFoundException when country is not found', async () => { + mockCountryService.findOneById.mockReturnValue(undefined); + + try { + await pipe.transform('12345'); + } catch (err: any) { + expect(err).toBeInstanceOf(NotFoundException); + } + }); + + it('Should be successful calls', async () => { + mockCountryService.findOneById.mockReturnValue(country); + + const result = await pipe.transform(country._id); + + expect(result).toBeDefined(); + expect(result).toBe(country); + }); + }); +}); diff --git a/test/modules/country/services/country.service.spec.ts b/test/modules/country/services/country.service.spec.ts new file mode 100644 index 000000000..17b2122ff --- /dev/null +++ b/test/modules/country/services/country.service.spec.ts @@ -0,0 +1,703 @@ +import { faker } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; +import { plainToInstance } from 'class-transformer'; +import mongoose from 'mongoose'; +import { DatabaseQueryContain } from 'src/common/database/decorators/database.decorator'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/enums/pagination.enum'; +import { CountryCreateRequestDto } from 'src/modules/country/dtos/request/country.create.request.dto'; +import { CountryGetResponseDto } from 'src/modules/country/dtos/response/country.get.response.dto'; +import { CountryListResponseDto } from 'src/modules/country/dtos/response/country.list.response.dto'; +import { CountryShortResponseDto } from 'src/modules/country/dtos/response/country.short.response.dto'; +import { + CountryDoc, + CountryEntity, + CountrySchema, +} from 'src/modules/country/repository/entities/country.entity'; +import { CountryRepository } from 'src/modules/country/repository/repositories/country.repository'; +import { CountryService } from 'src/modules/country/services/country.service'; +describe('CountryService', () => { + let service: CountryService; + + const countryId = faker.string.uuid(); + + const countryEntity: CountryEntity = { + _id: countryId, + name: 'Indonesia', + alpha2Code: 'ID', + alpha3Code: 'IDN', + domain: 'id', + fipsCode: 'ID', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Jakarta', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deleted: false, + } as CountryEntity; + + const countryDoc: CountryDoc = { + ...countryEntity, + save: jest.fn() as unknown, + toObject: jest.fn().mockReturnValue(countryEntity) as unknown, + toJSON: jest.fn().mockReturnValue(countryEntity) as unknown, + } as CountryDoc; + + const countryOtherEntity: CountryEntity = { + _id: faker.string.uuid(), + name: 'Singapore', + alpha2Code: 'SG', + alpha3Code: 'SGD', + domain: 'sg', + fipsCode: 'SG', + numericCode: '360', + phoneCode: ['62'], + continent: 'Asia', + timeZone: 'Asia/Singapore', + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + deleted: false, + } as CountryEntity; + + const countryOtherDoc: CountryDoc = { + ...countryOtherEntity, + save: jest.fn() as unknown, + toObject: jest.fn().mockReturnValue(countryOtherEntity) as unknown, + toJSON: jest.fn().mockReturnValue(countryOtherEntity) as unknown, + } as CountryDoc; + + const countriesEntity: CountryEntity[] = [ + countryEntity, + countryOtherEntity, + ]; + const countriesDoc: CountryDoc[] = [countryDoc, countryOtherDoc]; + + const mockCountryRepository = { + findAll: jest.fn(), + findOne: jest.fn(), + findOneById: jest.fn(), + getTotal: jest.fn(), + softDelete: jest.fn(), + deleteMany: jest.fn(), + createMany: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CountryService, + { provide: CountryRepository, useValue: mockCountryRepository }, + ], + }).compile(); + + service = module.get(CountryService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findAll', () => { + it('should return the data', async () => { + mockCountryRepository.findAll.mockReturnValue(countriesDoc); + const result = await service.findAll({}); + expect(mockCountryRepository.findAll).toHaveBeenCalledWith( + {}, + undefined + ); + expect(result.length).toEqual(2); + expect(result).toEqual(countriesDoc); + }); + + it('should return the data with options', async () => { + mockCountryRepository.findAll.mockReturnValue([countryDoc]); + const result = await service.findAll( + {}, + { + paging: { + limit: 1, + offset: 0, + }, + select: { + _id: true, + }, + order: { + _id: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }, + } + ); + expect(mockCountryRepository.findAll).toHaveBeenCalledWith( + {}, + { + paging: { + limit: 1, + offset: 0, + }, + select: { + _id: true, + }, + order: { + _id: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + }, + } + ); + expect(result.length).toEqual(1); + }); + }); + + describe('findOne', () => { + it('should find one a country by alpha3Code', async () => { + mockCountryRepository.findOne.mockImplementation((query: any) => + countriesDoc.find(e => e.alpha3Code === query.alpha3Code) + ); + + const result = await service.findOne({ + alpha3Code: 'IDN', + }); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + alpha3Code: 'IDN', + }, + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a country by alpha3Code with options select', async () => { + mockCountryRepository.findOne.mockImplementation((query: any) => + countriesDoc.find(e => e.alpha3Code === query.alpha3Code) + ); + + const result = await service.findOne( + { + alpha3Code: 'IDN', + }, + { + select: { + _id: true, + }, + } + ); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + alpha3Code: 'IDN', + }, + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('findOneByName', () => { + it('should find one a country by name', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneByName('Indonesia'); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + DatabaseQueryContain('name', 'Indonesia'), + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a country by name with options select', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneByName('Indonesia', { + select: { + _id: true, + }, + }); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + DatabaseQueryContain('name', 'Indonesia'), + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('findOneByAlpha2', () => { + it('should find one a country by alpha2', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneByAlpha2('ID'); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + DatabaseQueryContain('alpha2Code', 'ID'), + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a country by alpha2 with options select', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneByAlpha2('ID', { + select: { + _id: true, + }, + }); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + DatabaseQueryContain('alpha2Code', 'ID'), + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('findOneActiveByPhoneCode', () => { + it('should find one a active country by phone number', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneActiveByPhoneCode('62'); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + phoneCode: '62', + isActive: true, + }, + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a active country by phone number with options select', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneActiveByPhoneCode('62', { + select: { + _id: true, + }, + }); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + phoneCode: '62', + isActive: true, + }, + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('findOneById', () => { + it('should find one a country by id', async () => { + mockCountryRepository.findOneById.mockReturnValue(countryDoc); + + const result = await service.findOneById(countryId); + + expect(mockCountryRepository.findOneById).toHaveBeenCalledWith( + countryId, + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a country by id with options select', async () => { + mockCountryRepository.findOneById.mockReturnValue(countryDoc); + + const result = await service.findOneById(countryId, { + select: { + _id: true, + }, + }); + + expect(mockCountryRepository.findOneById).toHaveBeenCalledWith( + countryId, + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('findOneActiveById', () => { + it('should find one a active country by id', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneActiveById(countryId); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + _id: countryId, + isActive: true, + }, + undefined + ); + expect(result).toEqual(countryDoc); + }); + + it('should find one a active country by id with options select', async () => { + mockCountryRepository.findOne.mockReturnValue(countryDoc); + + const result = await service.findOneActiveById(countryId, { + select: { + _id: true, + }, + }); + + expect(mockCountryRepository.findOne).toHaveBeenCalledWith( + { + _id: countryId, + isActive: true, + }, + { + select: { + _id: true, + }, + } + ); + expect(result).toEqual(countryDoc); + }); + }); + + describe('getTotal', () => { + it('should get total number of countries', async () => { + mockCountryRepository.getTotal.mockImplementation( + () => countriesDoc.length + ); + + const result = await service.getTotal({}); + + expect(mockCountryRepository.getTotal).toHaveBeenCalledWith( + {}, + undefined + ); + expect(typeof result).toBe('number'); + expect(result).toBe(countriesDoc.length); + }); + + it('should get total number of countries with options', async () => { + mockCountryRepository.getTotal.mockImplementation( + () => countriesDoc.length + ); + + const result = await service.getTotal( + {}, + { + withDeleted: true, + } + ); + + expect(mockCountryRepository.getTotal).toHaveBeenCalledWith( + {}, + { + withDeleted: true, + } + ); + expect(typeof result).toBe('number'); + expect(result).toBe(countriesDoc.length); + }); + }); + + describe('createMany', () => { + it('should create many country', async () => { + const dto: CountryCreateRequestDto[] = [ + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + ]; + const entity = dto.map( + ({ + name, + alpha2Code, + alpha3Code, + numericCode, + continent, + fipsCode, + phoneCode, + timeZone, + domain, + }): CountryCreateRequestDto => { + const create: CountryEntity = new CountryEntity(); + create.name = name; + create.alpha2Code = alpha2Code; + create.alpha3Code = alpha3Code; + create.numericCode = numericCode; + create.continent = continent; + create.fipsCode = fipsCode; + create.phoneCode = phoneCode; + create.timeZone = timeZone; + create.domain = domain; + + return create; + } + ); + + mockCountryRepository.createMany.mockResolvedValue(true); + + const result = await service.createMany(dto); + + expect(mockCountryRepository.createMany).toHaveBeenCalledWith( + entity, + undefined + ); + expect(result).toBe(true); + }); + + it('should create many country', async () => { + const dto: CountryCreateRequestDto[] = [ + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + ]; + const entity = dto.map( + ({ + name, + alpha2Code, + alpha3Code, + numericCode, + continent, + fipsCode, + phoneCode, + timeZone, + domain, + }): CountryCreateRequestDto => { + const create: CountryEntity = new CountryEntity(); + create.name = name; + create.alpha2Code = alpha2Code; + create.alpha3Code = alpha3Code; + create.numericCode = numericCode; + create.continent = continent; + create.fipsCode = fipsCode; + create.phoneCode = phoneCode; + create.timeZone = timeZone; + create.domain = domain; + + return create; + } + ); + + mockCountryRepository.createMany.mockResolvedValue(true); + + const session: any = jest.fn(); + const result = await service.createMany(dto, { session }); + + expect(mockCountryRepository.createMany).toHaveBeenCalledWith( + entity, + { + session, + } + ); + expect(result).toBe(true); + }); + + it('should throw an error', async () => { + const dto: CountryCreateRequestDto[] = [ + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + { + name: faker.location.country(), + alpha2Code: faker.location.countryCode(), + alpha3Code: faker.location.countryCode(), + numericCode: faker.location.countryCode(), + continent: faker.location.county(), + fipsCode: faker.location.countryCode(), + phoneCode: ['62'], + timeZone: faker.lorem.word(), + domain: faker.internet.domainSuffix(), + }, + ]; + mockCountryRepository.createMany.mockRejectedValue( + new Error('test error') + ); + + try { + await service.createMany(dto); + } catch (err: any) { + expect(err).toBeInstanceOf(Error); + expect(err).toEqual(new Error('test error')); + } + }); + }); + + describe('deleteMany', () => { + it('should delete many countries', async () => { + mockCountryRepository.deleteMany.mockResolvedValue(true); + + const result = await service.deleteMany({}); + + expect(mockCountryRepository.deleteMany).toHaveBeenCalledWith( + {}, + undefined + ); + expect(result).toBe(true); + }); + + it('should delete many countries with options', async () => { + mockCountryRepository.deleteMany.mockResolvedValue(true); + + const session: any = jest.fn(); + const result = await service.deleteMany({}, { session }); + + expect(mockCountryRepository.deleteMany).toHaveBeenCalledWith( + {}, + { session } + ); + expect(result).toBe(true); + }); + + it('should throw an error', async () => { + mockCountryRepository.deleteMany.mockRejectedValue( + new Error('test error') + ); + + const session: any = jest.fn(); + + try { + await service.deleteMany({}, { session }); + } catch (err: any) { + expect(err).toBeInstanceOf(Error); + expect(err).toEqual(new Error('test error')); + } + }); + }); + + describe('mapList', () => { + it('should map list docs to response dto', async () => { + const CountryDocTest = mongoose.model('test', CountrySchema); + const docsTest = countriesEntity.map(e => new CountryDocTest(e)); + const result = await service.mapList(docsTest); + const mapped = plainToInstance( + CountryListResponseDto, + countriesEntity + ); + + expect(result).toEqual(mapped); + }); + + it('should map list entities to response dto', async () => { + const result = await service.mapList(countriesEntity); + const mapped = plainToInstance( + CountryListResponseDto, + countriesEntity + ); + + expect(result).toEqual(mapped); + }); + }); + + describe('mapGet', () => { + it('should map one docs to response dto', async () => { + const CountryDocTest = mongoose.model('test', CountrySchema); + const docTest = new CountryDocTest(countryEntity); + const result = await service.mapGet(docTest); + const mapped = plainToInstance( + CountryGetResponseDto, + countryEntity + ); + + expect(result).toEqual(mapped); + }); + + it('should map one entities to response dto', async () => { + const result = await service.mapGet(countryEntity); + const mapped = plainToInstance( + CountryGetResponseDto, + countryEntity + ); + + expect(result).toEqual(mapped); + }); + }); + + describe('mapShort', () => { + it('should map list docs to response dto', async () => { + const CountryDocTest = mongoose.model('test', CountrySchema); + const docsTest = countriesEntity.map(e => new CountryDocTest(e)); + const result = await service.mapShort(docsTest); + const mapped = plainToInstance( + CountryShortResponseDto, + countriesEntity + ); + + expect(result).toEqual(mapped); + }); + + it('should map list entities to response dto', async () => { + const result = await service.mapShort(countriesEntity); + const mapped = plainToInstance( + CountryShortResponseDto, + countriesEntity + ); + + expect(result).toEqual(mapped); + }); + }); +}); diff --git a/test/modules/policy/decorators/policy.decorator.spec.ts b/test/modules/policy/decorators/policy.decorator.spec.ts new file mode 100644 index 000000000..b931886d9 --- /dev/null +++ b/test/modules/policy/decorators/policy.decorator.spec.ts @@ -0,0 +1,58 @@ +import { SetMetadata, UseGuards } from '@nestjs/common'; +import { + POLICY_ABILITY_META_KEY, + POLICY_ROLE_META_KEY, +} from 'src/modules/policy/constants/policy.constant'; +import { + PolicyAbilityProtected, + PolicyRoleProtected, +} from 'src/modules/policy/decorators/policy.decorator'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { PolicyAbilityGuard } from 'src/modules/policy/guards/policy.ability.guard'; +import { PolicyRoleGuard } from 'src/modules/policy/guards/policy.role.guard'; + +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + UseGuards: jest.fn(), + SetMetadata: jest.fn(), +})); + +describe('Policy Decorators', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('PolicyAbilityProtected', () => { + it('Should return applyDecorators with property', async () => { + const result = PolicyAbilityProtected({ + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ENUM_POLICY_ACTION.READ], + }); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(PolicyAbilityGuard); + expect(SetMetadata).toHaveBeenCalledWith(POLICY_ABILITY_META_KEY, [ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ENUM_POLICY_ACTION.READ], + }, + ]); + }); + }); + + describe('PolicyRoleProtected', () => { + it('Should return applyDecorators with property', async () => { + const result = PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.ADMIN); + + expect(result).toBeTruthy(); + expect(UseGuards).toHaveBeenCalledWith(PolicyRoleGuard); + expect(SetMetadata).toHaveBeenCalledWith(POLICY_ROLE_META_KEY, [ + ENUM_POLICY_ROLE_TYPE.ADMIN, + ]); + }); + }); +}); diff --git a/test/modules/policy/factories/policy.factory.spec.ts b/test/modules/policy/factories/policy.factory.spec.ts new file mode 100644 index 000000000..b30fa2225 --- /dev/null +++ b/test/modules/policy/factories/policy.factory.spec.ts @@ -0,0 +1,160 @@ +import { AuthJwtAccessPayloadPermissionDto } from 'src/modules/auth/dtos/jwt/auth.jwt.access-payload.dto'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { PolicyAbilityFactory } from 'src/modules/policy/factories/policy.factory'; + +describe('PolicyAbilityFactory', () => { + let factory: PolicyAbilityFactory; + + beforeEach(async () => { + factory = new PolicyAbilityFactory(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(factory).toBeDefined(); + }); + + describe('defineFromRequest', () => { + it('should define user ability, user can Read and Create a Api Key', () => { + const permissionPayload: AuthJwtAccessPayloadPermissionDto[] = [ + { + action: '1,2', + subject: ENUM_POLICY_SUBJECT.API_KEY, + }, + ]; + + const ability = factory.defineFromRequest(permissionPayload); + expect( + ability.can( + ENUM_POLICY_ACTION.READ, + ENUM_POLICY_SUBJECT.API_KEY + ) + ).toBe(true); + expect( + ability.can( + ENUM_POLICY_ACTION.CREATE, + ENUM_POLICY_SUBJECT.API_KEY + ) + ).toBe(true); + }); + + it('should define all ability', () => { + const permissionPayload: AuthJwtAccessPayloadPermissionDto[] = [ + { + action: '0', + subject: ENUM_POLICY_SUBJECT.ALL, + }, + ]; + + const ability = factory.defineFromRequest(permissionPayload); + expect(ability.can(ENUM_POLICY_ACTION.MANAGE, 'all')).toBe(true); + }); + }); + + describe('mappingFromRequest', () => { + it('should map ability of user', () => { + const permissionPayload: AuthJwtAccessPayloadPermissionDto = { + action: '1,2', + subject: ENUM_POLICY_SUBJECT.API_KEY, + }; + + const ability = factory.mappingFromRequest(permissionPayload); + expect(Array.isArray(ability)).toBe(true); + expect(ability.length).toBe(2); + expect(ability).toEqual([ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: ENUM_POLICY_ACTION.READ, + }, + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: ENUM_POLICY_ACTION.CREATE, + }, + ]); + }); + }); + + describe('mapping', () => { + it('should convert action 0 from request to enum policy MANAGE', () => { + const mapping = factory.mapping(0); + + expect(mapping).toBe(ENUM_POLICY_ACTION.MANAGE); + }); + + it('should convert action 1 from request to enum policy READ', () => { + const mapping = factory.mapping(1); + + expect(mapping).toBe(ENUM_POLICY_ACTION.READ); + }); + + it('should convert action 2 from request to enum policy CREATE', () => { + const mapping = factory.mapping(2); + + expect(mapping).toBe(ENUM_POLICY_ACTION.CREATE); + }); + + it('should convert action 3 from request to enum policy UPDATE', () => { + const mapping = factory.mapping(3); + + expect(mapping).toBe(ENUM_POLICY_ACTION.UPDATE); + }); + + it('should convert action 4 from request to enum policy DELETE', () => { + const mapping = factory.mapping(4); + + expect(mapping).toBe(ENUM_POLICY_ACTION.DELETE); + }); + + it('should convert action 5 from request to enum policy EXPORT', () => { + const mapping = factory.mapping(5); + + expect(mapping).toBe(ENUM_POLICY_ACTION.EXPORT); + }); + + it('should convert action 6 from request to enum policy IMPORT', () => { + const mapping = factory.mapping(6); + + expect(mapping).toBe(ENUM_POLICY_ACTION.IMPORT); + }); + + it('should convert action undefined from request to null', () => { + const mapping = factory.mapping(undefined); + + expect(mapping).toBe(null); + }); + }); + + describe('handlerAbilities', () => { + it('should return handle rules for policy', () => { + const handlers = factory.handlerAbilities([ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ + ENUM_POLICY_ACTION.READ, + ENUM_POLICY_ACTION.CREATE, + ], + }, + ]); + + expect(Array.isArray(handlers)).toBe(true); + expect(handlers.length).toBe(2); + + const permissionPayload: AuthJwtAccessPayloadPermissionDto[] = [ + { + action: '1', + subject: ENUM_POLICY_SUBJECT.API_KEY, + }, + ]; + + const ability = factory.defineFromRequest(permissionPayload); + const handler: any = handlers[0]; + expect(handler(ability)).toBe(true); + }); + }); +}); diff --git a/test/modules/policy/guards/policy.ability.guard.spec.ts b/test/modules/policy/guards/policy.ability.guard.spec.ts new file mode 100644 index 000000000..61ecb4f57 --- /dev/null +++ b/test/modules/policy/guards/policy.ability.guard.spec.ts @@ -0,0 +1,201 @@ +import { createMock } from '@golevelup/ts-jest'; +import { + ExecutionContext, + ForbiddenException, + HttpException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { POLICY_ABILITY_META_KEY } from 'src/modules/policy/constants/policy.constant'; +import { + ENUM_POLICY_ACTION, + ENUM_POLICY_ROLE_TYPE, + ENUM_POLICY_SUBJECT, +} from 'src/modules/policy/enums/policy.enum'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; +import { PolicyAbilityFactory } from 'src/modules/policy/factories/policy.factory'; +import { PolicyAbilityGuard } from 'src/modules/policy/guards/policy.ability.guard'; +import { IPolicyAbility } from 'src/modules/policy/interfaces/policy.interface'; + +describe('PolicyAbilityGuard', () => { + let guard: PolicyAbilityGuard; + + const user = { + type: ENUM_POLICY_ROLE_TYPE.ADMIN, + permissions: [ + { + action: '1,2', + subject: ENUM_POLICY_SUBJECT.API_KEY, + }, + ], + }; + + const userForbidden = { + type: ENUM_POLICY_ROLE_TYPE.ADMIN, + permissions: [ + { + action: '1,2', + subject: ENUM_POLICY_SUBJECT.ROLE, + }, + ], + }; + + const userSuperAdmin = { + type: ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN, + permissions: [], + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [PolicyAbilityGuard, PolicyAbilityFactory], + }).compile(); + + guard = moduleRef.get(PolicyAbilityGuard); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + describe('canActivate', () => { + it('Should passed the guard', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ABILITY_META_KEY: + return [ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ENUM_POLICY_ACTION.READ], + }, + ]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user, + }), + }), + }); + + const result = await guard.canActivate(executionContext); + + expect(result).toBe(true); + }); + + it('Should passed the guard without predefined ability', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ABILITY_META_KEY: + return undefined; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user, + }), + }), + }); + + try { + await guard.canActivate(executionContext); + } catch (err) { + expect(err).toBeInstanceOf(ForbiddenException); + + const response: Record = ( + err as HttpException + ).getResponse() as Record; + expect(response.statusCode).toBe( + ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_PREDEFINED_NOT_FOUND + ); + expect(response.message).toBe( + 'policy.error.abilityPredefinedNotFound' + ); + } + }); + + it('Should passed the guard because of super admin', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ABILITY_META_KEY: + return [ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ENUM_POLICY_ACTION.READ], + }, + ] as IPolicyAbility[]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: userSuperAdmin, + }), + }), + }); + + const result = await guard.canActivate(executionContext); + + expect(result).toBe(true); + }); + + it('Should throw ForbiddenException error', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ABILITY_META_KEY: + return [ + { + subject: ENUM_POLICY_SUBJECT.API_KEY, + action: [ENUM_POLICY_ACTION.READ], + }, + ] as IPolicyAbility[]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: userForbidden, + }), + }), + }); + + try { + await guard.canActivate(executionContext); + } catch (err) { + expect(err).toBeInstanceOf(ForbiddenException); + + const response: Record = ( + err as HttpException + ).getResponse() as Record; + expect(response.statusCode).toBe( + ENUM_POLICY_STATUS_CODE_ERROR.ABILITY_FORBIDDEN + ); + expect(response.message).toBe('policy.error.abilityForbidden'); + } + }); + }); +}); diff --git a/test/modules/policy/guards/policy.role.guard.spec.ts b/test/modules/policy/guards/policy.role.guard.spec.ts new file mode 100644 index 000000000..5b0ef5ad2 --- /dev/null +++ b/test/modules/policy/guards/policy.role.guard.spec.ts @@ -0,0 +1,165 @@ +import { createMock } from '@golevelup/ts-jest'; +import { + ExecutionContext, + ForbiddenException, + HttpException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { POLICY_ROLE_META_KEY } from 'src/modules/policy/constants/policy.constant'; +import { ENUM_POLICY_ROLE_TYPE } from 'src/modules/policy/enums/policy.enum'; +import { ENUM_POLICY_STATUS_CODE_ERROR } from 'src/modules/policy/enums/policy.status-code.enum'; +import { PolicyRoleGuard } from 'src/modules/policy/guards/policy.role.guard'; + +describe('PolicyRoleGuard', () => { + let guard: PolicyRoleGuard; + + const user = { + type: ENUM_POLICY_ROLE_TYPE.ADMIN, + }; + + const userForbidden = { + type: ENUM_POLICY_ROLE_TYPE.ADMIN, + }; + + const userSuperAdmin = { + type: ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN, + }; + + beforeEach(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: [PolicyRoleGuard], + }).compile(); + + guard = moduleRef.get(PolicyRoleGuard); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + describe('canActivate', () => { + it('Should passed the guard', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ROLE_META_KEY: + return [ENUM_POLICY_ROLE_TYPE.ADMIN]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user, + }), + }), + }); + const result = await guard.canActivate(executionContext); + expect(result).toBe(true); + }); + + it('Should passed the guard without predefined ability', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ROLE_META_KEY: + return undefined; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user, + }), + }), + }); + + try { + await guard.canActivate(executionContext); + } catch (err) { + expect(err).toBeInstanceOf(ForbiddenException); + + const response: Record = ( + err as HttpException + ).getResponse() as Record; + expect(response.statusCode).toBe( + ENUM_POLICY_STATUS_CODE_ERROR.ROLE_PREDEFINED_NOT_FOUND + ); + expect(response.message).toBe( + 'policy.error.rolePredefinedNotFound' + ); + } + }); + + it('Should passed the guard because of super admin', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ROLE_META_KEY: + return [ENUM_POLICY_ROLE_TYPE.ADMIN]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: userSuperAdmin, + }), + }), + }); + + const result = await guard.canActivate(executionContext); + + expect(result).toBe(true); + }); + + it('Should throw ForbiddenException error', async () => { + jest.spyOn(guard['reflector'], 'get').mockImplementation( + (key: string) => { + switch (key) { + case POLICY_ROLE_META_KEY: + return [ENUM_POLICY_ROLE_TYPE.SUPER_ADMIN]; + default: + return true; + } + } + ); + + const executionContext = createMock({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: userForbidden, + }), + }), + }); + + try { + await guard.canActivate(executionContext); + } catch (err) { + expect(err).toBeInstanceOf(ForbiddenException); + + const response: Record = ( + err as HttpException + ).getResponse() as Record; + expect(response.statusCode).toBe( + ENUM_POLICY_STATUS_CODE_ERROR.ROLE_FORBIDDEN + ); + expect(response.message).toBe('policy.error.roleForbidden'); + } + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8aa9bfd93..f39f73c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,27 +26,27 @@ rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/core@17.1.2": - version "17.1.2" - resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.2.tgz" - integrity sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw== +"@angular-devkit/core@17.3.8": + version "17.3.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.3.8.tgz#8679cacf84cf79764f027811020e235ab32016d2" + integrity sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q== dependencies: ajv "8.12.0" ajv-formats "2.1.1" - jsonc-parser "3.2.0" - picomatch "3.0.1" + jsonc-parser "3.2.1" + picomatch "4.0.1" rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/schematics-cli@17.1.2": - version "17.1.2" - resolved "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.1.2.tgz" - integrity sha512-bvXykYzSST05qFdlgIzUguNOb3z0hCa8HaTwtqdmQo9aFPf+P+/AC56I64t1iTchMjQtf3JrBQhYM25gUdcGbg== +"@angular-devkit/schematics-cli@17.3.8": + version "17.3.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-17.3.8.tgz#26eeb9b581309be474868d01d9f87555760557c3" + integrity sha512-TjmiwWJarX7oqvNiRAroQ5/LeKUatxBOCNEuKXO/PV8e7pn/Hr/BqfFm+UcYrQoFdZplmtNAfqmbqgVziKvCpA== dependencies: - "@angular-devkit/core" "17.1.2" - "@angular-devkit/schematics" "17.1.2" + "@angular-devkit/core" "17.3.8" + "@angular-devkit/schematics" "17.3.8" ansi-colors "4.1.3" - inquirer "9.2.12" + inquirer "9.2.15" symbol-observable "4.0.0" yargs-parser "21.1.1" @@ -61,14 +61,14 @@ ora "5.4.1" rxjs "7.8.1" -"@angular-devkit/schematics@17.1.2": - version "17.1.2" - resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.2.tgz" - integrity sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA== +"@angular-devkit/schematics@17.3.8": + version "17.3.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.3.8.tgz#f853eb21682aadfb6667e090b5b509fc95ce8442" + integrity sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg== dependencies: - "@angular-devkit/core" "17.1.2" - jsonc-parser "3.2.0" - magic-string "0.30.5" + "@angular-devkit/core" "17.3.8" + jsonc-parser "3.2.1" + magic-string "0.30.8" ora "5.4.1" rxjs "7.8.1" @@ -140,525 +140,533 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-sdk/client-s3@^3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.598.0.tgz#04f594cb7e36b3551898c656715f94785ca8acf0" - integrity sha512-UMxftsgF6j1vzm4Qd9vQJHs2he1NQCWWV8esZfmNFq23OpUC2BPMxkqi13ZQ9tnTAZUNs7yFT/x4Zsi/wpRZEw== +"@aws-sdk/client-s3@^3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.629.0.tgz#6c22639c0cdb73b05409b5633a87010bfec7b107" + integrity sha512-Q0YXKdUA7NboPl94JOKD4clHHuERG1Kwy0JPbU+3Hvmz/UuwUGBmlfaRAqd9y4LXsTv/2xKtFPW9R+nBfy9mwA== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.598.0" - "@aws-sdk/client-sts" "3.598.0" - "@aws-sdk/core" "3.598.0" - "@aws-sdk/credential-provider-node" "3.598.0" - "@aws-sdk/middleware-bucket-endpoint" "3.598.0" - "@aws-sdk/middleware-expect-continue" "3.598.0" - "@aws-sdk/middleware-flexible-checksums" "3.598.0" - "@aws-sdk/middleware-host-header" "3.598.0" - "@aws-sdk/middleware-location-constraint" "3.598.0" - "@aws-sdk/middleware-logger" "3.598.0" - "@aws-sdk/middleware-recursion-detection" "3.598.0" - "@aws-sdk/middleware-sdk-s3" "3.598.0" - "@aws-sdk/middleware-signing" "3.598.0" - "@aws-sdk/middleware-ssec" "3.598.0" - "@aws-sdk/middleware-user-agent" "3.598.0" - "@aws-sdk/region-config-resolver" "3.598.0" - "@aws-sdk/signature-v4-multi-region" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@aws-sdk/util-user-agent-browser" "3.598.0" - "@aws-sdk/util-user-agent-node" "3.598.0" - "@aws-sdk/xml-builder" "3.598.0" - "@smithy/config-resolver" "^3.0.2" - "@smithy/core" "^2.2.1" - "@smithy/eventstream-serde-browser" "^3.0.2" - "@smithy/eventstream-serde-config-resolver" "^3.0.1" - "@smithy/eventstream-serde-node" "^3.0.2" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/hash-blob-browser" "^3.1.0" - "@smithy/hash-node" "^3.0.1" - "@smithy/hash-stream-node" "^3.1.0" - "@smithy/invalid-dependency" "^3.0.1" - "@smithy/md5-js" "^3.0.1" - "@smithy/middleware-content-length" "^3.0.1" - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@aws-sdk/client-sso-oidc" "3.629.0" + "@aws-sdk/client-sts" "3.629.0" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/credential-provider-node" "3.629.0" + "@aws-sdk/middleware-bucket-endpoint" "3.620.0" + "@aws-sdk/middleware-expect-continue" "3.620.0" + "@aws-sdk/middleware-flexible-checksums" "3.620.0" + "@aws-sdk/middleware-host-header" "3.620.0" + "@aws-sdk/middleware-location-constraint" "3.609.0" + "@aws-sdk/middleware-logger" "3.609.0" + "@aws-sdk/middleware-recursion-detection" "3.620.0" + "@aws-sdk/middleware-sdk-s3" "3.629.0" + "@aws-sdk/middleware-ssec" "3.609.0" + "@aws-sdk/middleware-user-agent" "3.620.0" + "@aws-sdk/region-config-resolver" "3.614.0" + "@aws-sdk/signature-v4-multi-region" "3.629.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@aws-sdk/util-user-agent-browser" "3.609.0" + "@aws-sdk/util-user-agent-node" "3.614.0" + "@aws-sdk/xml-builder" "3.609.0" + "@smithy/config-resolver" "^3.0.5" + "@smithy/core" "^2.3.2" + "@smithy/eventstream-serde-browser" "^3.0.6" + "@smithy/eventstream-serde-config-resolver" "^3.0.3" + "@smithy/eventstream-serde-node" "^3.0.5" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/hash-blob-browser" "^3.1.2" + "@smithy/hash-node" "^3.0.3" + "@smithy/hash-stream-node" "^3.1.2" + "@smithy/invalid-dependency" "^3.0.3" + "@smithy/md5-js" "^3.0.3" + "@smithy/middleware-content-length" "^3.0.5" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" "@smithy/util-base64" "^3.0.0" "@smithy/util-body-length-browser" "^3.0.0" "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.4" - "@smithy/util-defaults-mode-node" "^3.0.4" - "@smithy/util-endpoints" "^2.0.2" - "@smithy/util-retry" "^3.0.1" - "@smithy/util-stream" "^3.0.2" + "@smithy/util-defaults-mode-browser" "^3.0.14" + "@smithy/util-defaults-mode-node" "^3.0.14" + "@smithy/util-endpoints" "^2.0.5" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" + "@smithy/util-stream" "^3.1.3" "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.0.1" + "@smithy/util-waiter" "^3.1.2" tslib "^2.6.2" -"@aws-sdk/client-ses@^3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.598.0.tgz#105eb84e60f0597f1363cb9a2e91acc0c6e3c8e5" - integrity sha512-6ef3b33lg76GHY0wxrMdI0nHSCp6MgkFO6n4piB2mbP+ydCNSSjHbpkOQPGthpcg37qXk1XLZynSMpaa8pDbBw== +"@aws-sdk/client-ses@^3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.629.0.tgz#e54e01aa38540b414b1546ad644bbb5bd11ccbec" + integrity sha512-KreCdUAO/gIzWCgnPV1/dGUvLDDTdXI3fZzjjHUWFa1bE4wENjenNnWGw0qZgc8xB8pgiMdgPn7N+JvxJ7c/ZQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.598.0" - "@aws-sdk/client-sts" "3.598.0" - "@aws-sdk/core" "3.598.0" - "@aws-sdk/credential-provider-node" "3.598.0" - "@aws-sdk/middleware-host-header" "3.598.0" - "@aws-sdk/middleware-logger" "3.598.0" - "@aws-sdk/middleware-recursion-detection" "3.598.0" - "@aws-sdk/middleware-user-agent" "3.598.0" - "@aws-sdk/region-config-resolver" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@aws-sdk/util-user-agent-browser" "3.598.0" - "@aws-sdk/util-user-agent-node" "3.598.0" - "@smithy/config-resolver" "^3.0.2" - "@smithy/core" "^2.2.1" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/hash-node" "^3.0.1" - "@smithy/invalid-dependency" "^3.0.1" - "@smithy/middleware-content-length" "^3.0.1" - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@aws-sdk/client-sso-oidc" "3.629.0" + "@aws-sdk/client-sts" "3.629.0" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/credential-provider-node" "3.629.0" + "@aws-sdk/middleware-host-header" "3.620.0" + "@aws-sdk/middleware-logger" "3.609.0" + "@aws-sdk/middleware-recursion-detection" "3.620.0" + "@aws-sdk/middleware-user-agent" "3.620.0" + "@aws-sdk/region-config-resolver" "3.614.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@aws-sdk/util-user-agent-browser" "3.609.0" + "@aws-sdk/util-user-agent-node" "3.614.0" + "@smithy/config-resolver" "^3.0.5" + "@smithy/core" "^2.3.2" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/hash-node" "^3.0.3" + "@smithy/invalid-dependency" "^3.0.3" + "@smithy/middleware-content-length" "^3.0.5" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" "@smithy/util-base64" "^3.0.0" "@smithy/util-body-length-browser" "^3.0.0" "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.4" - "@smithy/util-defaults-mode-node" "^3.0.4" - "@smithy/util-endpoints" "^2.0.2" - "@smithy/util-middleware" "^3.0.1" - "@smithy/util-retry" "^3.0.1" + "@smithy/util-defaults-mode-browser" "^3.0.14" + "@smithy/util-defaults-mode-node" "^3.0.14" + "@smithy/util-endpoints" "^2.0.5" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.0.1" + "@smithy/util-waiter" "^3.1.2" tslib "^2.6.2" -"@aws-sdk/client-sso-oidc@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.598.0.tgz#17ad1acd1c616ccbd36cda2db1ee80d63ad0aff5" - integrity sha512-jfdH1pAO9Tt8Nkta/JJLoUnwl7jaRdxToQTJfUtE+o3+0JP5sA4LfC2rBkJSWcU5BdAA+kyOs5Lv776DlN04Vg== +"@aws-sdk/client-sso-oidc@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.629.0.tgz#8bd4138c4ff24962e0f2753cfa9722a18330ad1f" + integrity sha512-3if0LauNJPqubGYf8vnlkp+B3yAeKRuRNxfNbHlE6l510xWGcKK/ZsEmiFmfePzKKSRrDh/cxMFMScgOrXptNg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sts" "3.598.0" - "@aws-sdk/core" "3.598.0" - "@aws-sdk/credential-provider-node" "3.598.0" - "@aws-sdk/middleware-host-header" "3.598.0" - "@aws-sdk/middleware-logger" "3.598.0" - "@aws-sdk/middleware-recursion-detection" "3.598.0" - "@aws-sdk/middleware-user-agent" "3.598.0" - "@aws-sdk/region-config-resolver" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@aws-sdk/util-user-agent-browser" "3.598.0" - "@aws-sdk/util-user-agent-node" "3.598.0" - "@smithy/config-resolver" "^3.0.2" - "@smithy/core" "^2.2.1" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/hash-node" "^3.0.1" - "@smithy/invalid-dependency" "^3.0.1" - "@smithy/middleware-content-length" "^3.0.1" - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/credential-provider-node" "3.629.0" + "@aws-sdk/middleware-host-header" "3.620.0" + "@aws-sdk/middleware-logger" "3.609.0" + "@aws-sdk/middleware-recursion-detection" "3.620.0" + "@aws-sdk/middleware-user-agent" "3.620.0" + "@aws-sdk/region-config-resolver" "3.614.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@aws-sdk/util-user-agent-browser" "3.609.0" + "@aws-sdk/util-user-agent-node" "3.614.0" + "@smithy/config-resolver" "^3.0.5" + "@smithy/core" "^2.3.2" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/hash-node" "^3.0.3" + "@smithy/invalid-dependency" "^3.0.3" + "@smithy/middleware-content-length" "^3.0.5" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" "@smithy/util-base64" "^3.0.0" "@smithy/util-body-length-browser" "^3.0.0" "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.4" - "@smithy/util-defaults-mode-node" "^3.0.4" - "@smithy/util-endpoints" "^2.0.2" - "@smithy/util-middleware" "^3.0.1" - "@smithy/util-retry" "^3.0.1" + "@smithy/util-defaults-mode-browser" "^3.0.14" + "@smithy/util-defaults-mode-node" "^3.0.14" + "@smithy/util-endpoints" "^2.0.5" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sso@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.598.0.tgz#aef58e198e504d3b3d1ba345355650a67d21facb" - integrity sha512-nOI5lqPYa+YZlrrzwAJywJSw3MKVjvu6Ge2fCqQUNYMfxFB0NAaDFnl0EPjXi+sEbtCuz/uWE77poHbqiZ+7Iw== +"@aws-sdk/client-sso@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.629.0.tgz#19ad0236cf3985da68552dc597ed14736450630e" + integrity sha512-2w8xU4O0Grca5HmT2dXZ5fF0g39RxODtmoqHJDsK5DSt750LqDG4w3ktmBvQs3+SrpkkJOjlX5v/hb2PCxVbww== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.598.0" - "@aws-sdk/middleware-host-header" "3.598.0" - "@aws-sdk/middleware-logger" "3.598.0" - "@aws-sdk/middleware-recursion-detection" "3.598.0" - "@aws-sdk/middleware-user-agent" "3.598.0" - "@aws-sdk/region-config-resolver" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@aws-sdk/util-user-agent-browser" "3.598.0" - "@aws-sdk/util-user-agent-node" "3.598.0" - "@smithy/config-resolver" "^3.0.2" - "@smithy/core" "^2.2.1" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/hash-node" "^3.0.1" - "@smithy/invalid-dependency" "^3.0.1" - "@smithy/middleware-content-length" "^3.0.1" - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/middleware-host-header" "3.620.0" + "@aws-sdk/middleware-logger" "3.609.0" + "@aws-sdk/middleware-recursion-detection" "3.620.0" + "@aws-sdk/middleware-user-agent" "3.620.0" + "@aws-sdk/region-config-resolver" "3.614.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@aws-sdk/util-user-agent-browser" "3.609.0" + "@aws-sdk/util-user-agent-node" "3.614.0" + "@smithy/config-resolver" "^3.0.5" + "@smithy/core" "^2.3.2" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/hash-node" "^3.0.3" + "@smithy/invalid-dependency" "^3.0.3" + "@smithy/middleware-content-length" "^3.0.5" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" "@smithy/util-base64" "^3.0.0" "@smithy/util-body-length-browser" "^3.0.0" "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.4" - "@smithy/util-defaults-mode-node" "^3.0.4" - "@smithy/util-endpoints" "^2.0.2" - "@smithy/util-middleware" "^3.0.1" - "@smithy/util-retry" "^3.0.1" + "@smithy/util-defaults-mode-browser" "^3.0.14" + "@smithy/util-defaults-mode-node" "^3.0.14" + "@smithy/util-endpoints" "^2.0.5" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sts@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.598.0.tgz#5b3c082ac14b3f0b7a4c964eb4ba2b320988e1e4" - integrity sha512-bXhz/cHL0iB9UH9IFtMaJJf4F8mV+HzncETCRFzZ9SyUMt5rP9j8A7VZknqGYSx/6mI8SsB1XJQkWSbhn6FiSQ== +"@aws-sdk/client-sts@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.629.0.tgz#a6ee546ebda64be90d310bb0a7316d98feabf1bd" + integrity sha512-RjOs371YwnSVGxhPjuluJKaxl4gcPYTAky0nPjwBime0i9/iS9nI8R8l5j7k7ec9tpFWjBPvNnThCU07pvjdzw== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.598.0" - "@aws-sdk/core" "3.598.0" - "@aws-sdk/credential-provider-node" "3.598.0" - "@aws-sdk/middleware-host-header" "3.598.0" - "@aws-sdk/middleware-logger" "3.598.0" - "@aws-sdk/middleware-recursion-detection" "3.598.0" - "@aws-sdk/middleware-user-agent" "3.598.0" - "@aws-sdk/region-config-resolver" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@aws-sdk/util-user-agent-browser" "3.598.0" - "@aws-sdk/util-user-agent-node" "3.598.0" - "@smithy/config-resolver" "^3.0.2" - "@smithy/core" "^2.2.1" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/hash-node" "^3.0.1" - "@smithy/invalid-dependency" "^3.0.1" - "@smithy/middleware-content-length" "^3.0.1" - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@aws-sdk/client-sso-oidc" "3.629.0" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/credential-provider-node" "3.629.0" + "@aws-sdk/middleware-host-header" "3.620.0" + "@aws-sdk/middleware-logger" "3.609.0" + "@aws-sdk/middleware-recursion-detection" "3.620.0" + "@aws-sdk/middleware-user-agent" "3.620.0" + "@aws-sdk/region-config-resolver" "3.614.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@aws-sdk/util-user-agent-browser" "3.609.0" + "@aws-sdk/util-user-agent-node" "3.614.0" + "@smithy/config-resolver" "^3.0.5" + "@smithy/core" "^2.3.2" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/hash-node" "^3.0.3" + "@smithy/invalid-dependency" "^3.0.3" + "@smithy/middleware-content-length" "^3.0.5" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" "@smithy/util-base64" "^3.0.0" "@smithy/util-body-length-browser" "^3.0.0" "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.4" - "@smithy/util-defaults-mode-node" "^3.0.4" - "@smithy/util-endpoints" "^2.0.2" - "@smithy/util-middleware" "^3.0.1" - "@smithy/util-retry" "^3.0.1" + "@smithy/util-defaults-mode-browser" "^3.0.14" + "@smithy/util-defaults-mode-node" "^3.0.14" + "@smithy/util-endpoints" "^2.0.5" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/core@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.598.0.tgz#82a069d703be0cafe3ddeacb1de51981ee4faa25" - integrity sha512-HaSjt7puO5Cc7cOlrXFCW0rtA0BM9lvzjl56x0A20Pt+0wxXGeTOZZOkXQIepbrFkV2e/HYukuT9e99vXDm59g== - dependencies: - "@smithy/core" "^2.2.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/signature-v4" "^3.1.0" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - fast-xml-parser "4.2.5" +"@aws-sdk/core@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.629.0.tgz#1ed02c657edcd22ffdce9b3b5bdbd2a36fe899aa" + integrity sha512-+/ShPU/tyIBM3oY1cnjgNA/tFyHtlWq+wXF9xEKRv19NOpYbWQ+xzNwVjGq8vR07cCRqy/sDQLWPhxjtuV/FiQ== + dependencies: + "@smithy/core" "^2.3.2" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/property-provider" "^3.1.3" + "@smithy/protocol-http" "^4.1.0" + "@smithy/signature-v4" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/util-middleware" "^3.0.3" + fast-xml-parser "4.4.1" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.598.0.tgz#ea1f30cfc9948017dd0608518868d3f50074164f" - integrity sha512-vi1khgn7yXzLCcgSIzQrrtd2ilUM0dWodxj3PQ6BLfP0O+q1imO3hG1nq7DVyJtq7rFHs6+9N8G4mYvTkxby2w== +"@aws-sdk/credential-provider-env@3.620.1": + version "3.620.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.620.1.tgz#d4692c49a65ebc11dae3f7f8b053fee9268a953c" + integrity sha512-ExuILJ2qLW5ZO+rgkNRj0xiAipKT16Rk77buvPP8csR7kkCflT/gXTyzRe/uzIiETTxM7tr8xuO9MP/DQXqkfg== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.598.0.tgz#58144440e698aef63b5cb459780325817c0acf10" - integrity sha512-N7cIafi4HVlQvEgvZSo1G4T9qb/JMLGMdBsDCT5XkeJrF0aptQWzTFH0jIdZcLrMYvzPcuEyO3yCBe6cy/ba0g== - dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/property-provider" "^3.1.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/util-stream" "^3.0.2" +"@aws-sdk/credential-provider-http@3.622.0": + version "3.622.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz#db481fdef859849d07dd5870894f45df2debab3d" + integrity sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg== + dependencies: + "@aws-sdk/types" "3.609.0" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/property-provider" "^3.1.3" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/util-stream" "^3.1.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.598.0.tgz#fd0ba8ab5c3701e05567d1c6f7752cfd9f4ba111" - integrity sha512-/ppcIVUbRwDIwJDoYfp90X3+AuJo2mvE52Y1t2VSrvUovYn6N4v95/vXj6LS8CNDhz2jvEJYmu+0cTMHdhI6eA== - dependencies: - "@aws-sdk/credential-provider-env" "3.598.0" - "@aws-sdk/credential-provider-http" "3.598.0" - "@aws-sdk/credential-provider-process" "3.598.0" - "@aws-sdk/credential-provider-sso" "3.598.0" - "@aws-sdk/credential-provider-web-identity" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@smithy/credential-provider-imds" "^3.1.1" - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" +"@aws-sdk/credential-provider-ini@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.629.0.tgz#88a88ec752d8db388300143a37e70d96d6ea2cef" + integrity sha512-r9fI7BABARvVDp77DBUImQzYdvarAIdhbvpCEZib0rlpvfWu3zxE9KZcapCAAi0MPjxeDfb7RMehFQIkAP7mYw== + dependencies: + "@aws-sdk/credential-provider-env" "3.620.1" + "@aws-sdk/credential-provider-http" "3.622.0" + "@aws-sdk/credential-provider-process" "3.620.1" + "@aws-sdk/credential-provider-sso" "3.629.0" + "@aws-sdk/credential-provider-web-identity" "3.621.0" + "@aws-sdk/types" "3.609.0" + "@smithy/credential-provider-imds" "^3.2.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.598.0.tgz#b24173cf9ae33718c6273c9bac996791c73d9359" - integrity sha512-sXTlqL5I/awlF9Dg2MQ17SfrEaABVnsj2mf4jF5qQrIRhfbvQOIYdEqdy8Rn1AWlJMz/N450SGzc0XJ5owxxqw== - dependencies: - "@aws-sdk/credential-provider-env" "3.598.0" - "@aws-sdk/credential-provider-http" "3.598.0" - "@aws-sdk/credential-provider-ini" "3.598.0" - "@aws-sdk/credential-provider-process" "3.598.0" - "@aws-sdk/credential-provider-sso" "3.598.0" - "@aws-sdk/credential-provider-web-identity" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@smithy/credential-provider-imds" "^3.1.1" - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" +"@aws-sdk/credential-provider-node@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.629.0.tgz#4004ada7d3edbf0d28c710a5a5d42027dc34bfb2" + integrity sha512-868hnVOLlXOBHk91Rl0jZIRgr/M4WJCa0nOrW9A9yidsQxuZp9P0vshDmm4hMvNZadmPIfo0Rra2MpA4RELoCw== + dependencies: + "@aws-sdk/credential-provider-env" "3.620.1" + "@aws-sdk/credential-provider-http" "3.622.0" + "@aws-sdk/credential-provider-ini" "3.629.0" + "@aws-sdk/credential-provider-process" "3.620.1" + "@aws-sdk/credential-provider-sso" "3.629.0" + "@aws-sdk/credential-provider-web-identity" "3.621.0" + "@aws-sdk/types" "3.609.0" + "@smithy/credential-provider-imds" "^3.2.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.598.0.tgz#f48ff6f964cd6726499b207f45bfecda4be922ce" - integrity sha512-rM707XbLW8huMk722AgjVyxu2tMZee++fNA8TJVNgs1Ma02Wx6bBrfIvlyK0rCcIRb0WdQYP6fe3Xhiu4e8IBA== +"@aws-sdk/credential-provider-process@3.620.1": + version "3.620.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.620.1.tgz#10387cf85400420bb4bbda9cc56937dcc6d6d0ee" + integrity sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.598.0.tgz#52781e2b60b1f61752829c44a5e0b9fedd0694d6" - integrity sha512-5InwUmrAuqQdOOgxTccRayMMkSmekdLk6s+az9tmikq0QFAHUCtofI+/fllMXSR9iL6JbGYi1940+EUmS4pHJA== - dependencies: - "@aws-sdk/client-sso" "3.598.0" - "@aws-sdk/token-providers" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" +"@aws-sdk/credential-provider-sso@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.629.0.tgz#f6c550d74007d1262149ae736df5868d4ea5aad7" + integrity sha512-Lf4XOuj6jamxgGZGrVojERh5S+NS2t2S4CUOnAu6tJ5U0GPlpjhINUKlcVxJBpsIXudMGW1nkumAd3+kazCPig== + dependencies: + "@aws-sdk/client-sso" "3.629.0" + "@aws-sdk/token-providers" "3.614.0" + "@aws-sdk/types" "3.609.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.598.0.tgz#d737e9c2b7c4460b8e31a55b4979bf4d88913900" - integrity sha512-GV5GdiMbz5Tz9JO4NJtRoFXjW0GPEujA0j+5J/B723rTN+REHthJu48HdBKouHGhdzkDWkkh1bu52V02Wprw8w== +"@aws-sdk/credential-provider-web-identity@3.621.0": + version "3.621.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.621.0.tgz#b25878c0a05dad60cd5f91e7e5a31a145c2f14be" + integrity sha512-w7ASSyfNvcx7+bYGep3VBgC3K6vEdLmlpjT7nSIHxxQf+WSdvy+HynwJosrpZax0sK5q0D1Jpn/5q+r5lwwW6w== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-bucket-endpoint@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.598.0.tgz#033b08921f9f284483a7337ed165743ee0dc598d" - integrity sha512-PM7BcFfGUSkmkT6+LU9TyJiB4S8yI7dfuKQDwK5ZR3P7MKaK4Uj4yyDiv0oe5xvkF6+O2+rShj+eh8YuWkOZ/Q== +"@aws-sdk/middleware-bucket-endpoint@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.620.0.tgz#c5dc0e98b6209a91479cad6c2c74fbc5a3429fab" + integrity sha512-eGLL0W6L3HDb3OACyetZYOWpHJ+gLo0TehQKeQyy2G8vTYXqNTeqYhuI6up9HVjBzU9eQiULVQETmgQs7TFaRg== dependencies: - "@aws-sdk/types" "3.598.0" + "@aws-sdk/types" "3.609.0" "@aws-sdk/util-arn-parser" "3.568.0" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-config-provider" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/middleware-expect-continue@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.598.0.tgz#5b08b8cae70d1e7cc082d3627b31856f6ba20d17" - integrity sha512-ZuHW18kaeHR8TQyhEOYMr8VwiIh0bMvF7J1OTqXHxDteQIavJWA3CbfZ9sgS4XGtrBZDyHJhjZKeCfLhN2rq3w== +"@aws-sdk/middleware-expect-continue@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.620.0.tgz#6a362c0f0696dc6749108a33de9998e0fa6b50ec" + integrity sha512-QXeRFMLfyQ31nAHLbiTLtk0oHzG9QLMaof5jIfqcUwnOkO8YnQdeqzakrg1Alpy/VQ7aqzIi8qypkBe2KXZz0A== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.598.0.tgz#8e40734d5fb1b116816f885885f16db9b5e39032" - integrity sha512-xukAzds0GQXvMEY9G6qt+CzwVzTx8NyKKh04O2Q+nOch6QQ8Rs+2kTRy3Z4wQmXq2pK9hlOWb5nXA7HWpmz6Ng== +"@aws-sdk/middleware-flexible-checksums@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.620.0.tgz#42cd48cdc0ad9639545be000bf537969210ce8c5" + integrity sha512-ftz+NW7qka2sVuwnnO1IzBku5ccP+s5qZGeRTPgrKB7OzRW85gthvIo1vQR2w+OwHFk7WJbbhhWwbCbktnP4UA== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" - "@aws-sdk/types" "3.598.0" + "@aws-sdk/types" "3.609.0" "@smithy/is-array-buffer" "^3.0.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/middleware-host-header@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.598.0.tgz#0a7c4d5a95657bea2d7c4e29b9a8b379952d09b1" - integrity sha512-WiaG059YBQwQraNejLIi0gMNkX7dfPZ8hDIhvMr5aVPRbaHH8AYF3iNSsXYCHvA2Cfa1O9haYXsuMF9flXnCmA== +"@aws-sdk/middleware-host-header@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz#b561d419a08a984ba364c193376b482ff5224d74" + integrity sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-location-constraint@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.598.0.tgz#45564d5119468e3ac97949431c249e8b6e00ec09" - integrity sha512-8oybQxN3F1ISOMULk7JKJz5DuAm5hCUcxMW9noWShbxTJuStNvuHf/WLUzXrf8oSITyYzIHPtf8VPlKR7I3orQ== +"@aws-sdk/middleware-location-constraint@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.609.0.tgz#7ed82d71e5ddcd50683ef2bbde10d1cc2492057e" + integrity sha512-xzsdoTkszGVqGVPjUmgoP7TORiByLueMHieI1fhQL888WPdqctwAx3ES6d/bA9Q/i8jnc6hs+Fjhy8UvBTkE9A== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-logger@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.598.0.tgz#0c0692d2f4f9007c915734ab319db377ca9a3b1b" - integrity sha512-bxBjf/VYiu3zfu8SYM2S9dQQc3tz5uBAOcPz/Bt8DyyK3GgOpjhschH/2XuUErsoUO1gDJqZSdGOmuHGZQn00Q== +"@aws-sdk/middleware-logger@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz#ed44d201f091b8bac908cbf14724c7a4d492553f" + integrity sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-recursion-detection@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.598.0.tgz#94015d41f8174bd41298fd13f8fb0a8c4576d7c8" - integrity sha512-vjT9BeFY9FeN0f8hm2l6F53tI0N5bUq6RcDkQXKNabXBnQxKptJRad6oP2X5y3FoVfBLOuDkQgiC2940GIPxtQ== +"@aws-sdk/middleware-recursion-detection@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.620.0.tgz#f8270dfff843fd756be971e5673f89c6a24c6513" + integrity sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.598.0.tgz#308604f8a38959ad65ec5674c643c7032d678f43" - integrity sha512-5AGtLAh9wyK6ANPYfaKTqJY1IFJyePIxsEbxa7zS6REheAqyVmgJFaGu3oQ5XlxfGr5Uq59tFTRkyx26G1HkHA== +"@aws-sdk/middleware-sdk-s3@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.629.0.tgz#10ad7b8af945f915d31f00cec0198248be95291c" + integrity sha512-FRXLcnPWXBoq/T9mnGnrpqhrSKNSm22rqJ0L7P14KESmbGuwhF/7ELYYxXIpgnIpb/CIUVmIU5EE8lsW1VTe8A== dependencies: - "@aws-sdk/types" "3.598.0" + "@aws-sdk/core" "3.629.0" + "@aws-sdk/types" "3.609.0" "@aws-sdk/util-arn-parser" "3.568.0" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/signature-v4" "^3.1.0" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" + "@smithy/core" "^2.3.2" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/signature-v4" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-stream" "^3.1.3" + "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/middleware-signing@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.598.0.tgz#b90eef6a9fe3f76777c9cd4890dcae8e1febd249" - integrity sha512-XKb05DYx/aBPqz6iCapsCbIl8aD8EihTuPCs51p75QsVfbQoVr4TlFfIl5AooMSITzojdAQqxt021YtvxjtxIQ== - dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/signature-v4" "^3.1.0" - "@smithy/types" "^3.1.0" - "@smithy/util-middleware" "^3.0.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-ssec@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.598.0.tgz#d6a3c64ce77bd7379653b46b58ded32a7b0fe6f4" - integrity sha512-f0p2xP8IC1uJ5e/tND1l81QxRtRFywEdnbtKCE0H6RSn4UIt2W3Dohe1qQDbnh27okF0PkNW6BJGdSAz3p7qbA== +"@aws-sdk/middleware-ssec@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.609.0.tgz#b87a8bc6133f3f6bdc6801183d0f9dad3f93cf9f" + integrity sha512-GZSD1s7+JswWOTamVap79QiDaIV7byJFssBW68GYjyRS5EBjNfwA/8s+6uE6g39R3ojyTbYOmvcANoZEhSULXg== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/middleware-user-agent@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.598.0.tgz#6fa26849d256434ca4884c42c1c4755aa2f1556e" - integrity sha512-4tjESlHG5B5MdjUaLK7tQs/miUtHbb6deauQx8ryqSBYOhfHVgb1ZnzvQR0bTrhpqUg0WlybSkDaZAICf9xctg== +"@aws-sdk/middleware-user-agent@3.620.0": + version "3.620.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.620.0.tgz#1fe3104f04f576a942cf0469bfbd73c38eef3d9e" + integrity sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A== dependencies: - "@aws-sdk/types" "3.598.0" - "@aws-sdk/util-endpoints" "3.598.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-endpoints" "3.614.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/region-config-resolver@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.598.0.tgz#fd8fd6b7bc11b5f81def4db0db9e835d40a8f86e" - integrity sha512-oYXhmTokSav4ytmWleCr3rs/1nyvZW/S0tdi6X7u+dLNL5Jee+uMxWGzgOrWK6wrQOzucLVjS4E/wA11Kv2GTw== +"@aws-sdk/region-config-resolver@3.614.0": + version "3.614.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz#9cebb31a5bcfea2a41891fff7f28d0164cde179a" + integrity sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/types" "^3.3.0" "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.1" + "@smithy/util-middleware" "^3.0.3" + tslib "^2.6.2" + +"@aws-sdk/s3-request-presigner@^3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.629.0.tgz#a8ab2aa3f59287baa7e7ef404fcdfe9bf201085a" + integrity sha512-6lVgK9Y5m+AqisPNLs1Low5oJHFg/lfsuEsQMKG5y0/uqR1KVLswiaY1mhp0cprMEXRN2DDMAhP7i+jy5/WLNw== + dependencies: + "@aws-sdk/signature-v4-multi-region" "3.629.0" + "@aws-sdk/types" "3.609.0" + "@aws-sdk/util-format-url" "3.609.0" + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.598.0.tgz#1716022e31dcbc5821aeca85204718f523a1ddbf" - integrity sha512-1r/EyTrO1gSa1FirnR8V7mabr7gk+l+HkyTI0fcTSr8ucB7gmYyW6WjkY8JCz13VYHFK62usCEDS7yoJoJOzTA== +"@aws-sdk/signature-v4-multi-region@3.629.0": + version "3.629.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.629.0.tgz#ca75443f3324fd398d228c3cba0f4275e7bb4a3a" + integrity sha512-GPX6dnmuLGDFp7CsGqGCzleEoNyr9ekgOzSBtcL5nKX++NruxO7f1QzJAbcYvz0gdKvz958UO0EKsGM6hnkTSg== dependencies: - "@aws-sdk/middleware-sdk-s3" "3.598.0" - "@aws-sdk/types" "3.598.0" - "@smithy/protocol-http" "^4.0.1" - "@smithy/signature-v4" "^3.1.0" - "@smithy/types" "^3.1.0" + "@aws-sdk/middleware-sdk-s3" "3.629.0" + "@aws-sdk/types" "3.609.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/signature-v4" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.598.0.tgz#49a94c14ce2e392bb0e84b69986c33ecfad5b804" - integrity sha512-TKY1EVdHVBnZqpyxyTHdpZpa1tUpb6nxVeRNn1zWG8QB5MvH4ALLd/jR+gtmWDNQbIG4cVuBOZFVL8hIYicKTA== +"@aws-sdk/token-providers@3.614.0": + version "3.614.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.614.0.tgz#88da04f6d4ce916b0b0f6e045676d04201fb47fd" + integrity sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/types@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.598.0.tgz#b840d2446dee19a2a4731e6166f2327915d846db" - integrity sha512-742uRl6z7u0LFmZwDrFP6r1wlZcgVPw+/TilluDJmCAR8BgRw3IR+743kUXKBGd8QZDRW2n6v/PYsi/AWCDDMQ== +"@aws-sdk/types@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.609.0.tgz#06b39d799c9f197a7b43670243e8e78a3bf7d6a5" + integrity sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@aws-sdk/types@^3.222.0": @@ -675,14 +683,24 @@ dependencies: tslib "^2.6.2" -"@aws-sdk/util-endpoints@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.598.0.tgz#7f78d68524babac7fdacf381590470353d45b959" - integrity sha512-Qo9UoiVVZxcOEdiOMZg3xb1mzkTxrhd4qSlg5QQrfWPJVx/QOg+Iy0NtGxPtHtVZNHZxohYwDwV/tfsnDSE2gQ== +"@aws-sdk/util-endpoints@3.614.0": + version "3.614.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz#6564b0ffd7dc3728221e9f9821f5aab1cc58468e" + integrity sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw== + dependencies: + "@aws-sdk/types" "3.609.0" + "@smithy/types" "^3.3.0" + "@smithy/util-endpoints" "^2.0.5" + tslib "^2.6.2" + +"@aws-sdk/util-format-url@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.609.0.tgz#f53907193bb636b52b61c81bbe6d7bd5ddc76c68" + integrity sha512-fuk29BI/oLQlJ7pfm6iJ4gkEpHdavffAALZwXh9eaY1vQ0ip0aKfRTiNudPoJjyyahnz5yJ1HkmlcDitlzsOrQ== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/types" "^3.1.0" - "@smithy/util-endpoints" "^2.0.2" + "@aws-sdk/types" "3.609.0" + "@smithy/querystring-builder" "^3.0.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": @@ -692,32 +710,32 @@ dependencies: tslib "^2.3.1" -"@aws-sdk/util-user-agent-browser@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.598.0.tgz#5039d0335f8a06af5be73c960df85009dda59090" - integrity sha512-36Sxo6F+ykElaL1mWzWjlg+1epMpSe8obwhCN1yGE7Js9ywy5U6k6l+A3q3YM9YRbm740sNxncbwLklMvuhTKw== +"@aws-sdk/util-user-agent-browser@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz#aa15421b2e32ae8bc589dac2bd6e8969832ce588" + integrity sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/types" "^3.3.0" bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.598.0.tgz#f9bdf1b7cc3a40787c379f7c2ff028de2612c177" - integrity sha512-oyWGcOlfTdzkC6SVplyr0AGh54IMrDxbhg5RxJ5P+V4BKfcDoDcZV9xenUk9NsOi9MuUjxMumb9UJGkDhM1m0A== +"@aws-sdk/util-user-agent-node@3.614.0": + version "3.614.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.614.0.tgz#1e3f49a80f841a3f21647baed2adce01aac5beb5" + integrity sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA== dependencies: - "@aws-sdk/types" "3.598.0" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@aws-sdk/types" "3.609.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/xml-builder@3.598.0": - version "3.598.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.598.0.tgz#ee591c5d80a34d9c5bc14326f1a62e9a0649c587" - integrity sha512-ZIa2RK7CHFTZ4gwK77WRtsZ6vF7xwRXxJ8KQIxK2duhoTVcn0xYxpFLdW9WZZZvdP9GIF3Loqvf8DRdeU5Jc7Q== +"@aws-sdk/xml-builder@3.609.0": + version "3.609.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.609.0.tgz#eeb3d5cde000a23cfeeefe0354b6193440dc7d87" + integrity sha512-l9XxNcA4HX98rwCC2/KoiWcmEiRfZe4G+mYwDbCFT87JIMj6GBhLDkAzr/W8KAaA2IDr8Vc6J8fZPgVulxxfMA== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.18.6": @@ -727,6 +745,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + "@babel/compat-data@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.6.tgz" @@ -762,6 +788,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" + integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== + dependencies: + "@babel/types" "^7.25.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz" @@ -832,11 +868,21 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz" integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz" @@ -860,11 +906,28 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.6.tgz" integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw== +"@babel/parser@^7.10.3", "@babel/parser@^7.25.0", "@babel/parser@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" + integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== + dependencies: + "@babel/types" "^7.25.2" + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" @@ -972,6 +1035,28 @@ "@babel/parser" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/traverse@^7.10.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" + integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.3" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.2" + debug "^4.3.1" + globals "^11.1.0" + "@babel/traverse@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.6.tgz" @@ -996,6 +1081,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.10.3", "@babel/types@^7.25.0", "@babel/types@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" + integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -1013,15 +1107,15 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@commitlint/cli@^19.3.0": - version "19.3.0" - resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.3.0.tgz#44e6da9823a01f0cdcc43054bbefdd2c6c5ddf39" - integrity sha512-LgYWOwuDR7BSTQ9OLZ12m7F/qhNY+NpAyPBgo4YNMkACE7lGuUnuQq1yi9hz1KA4+3VqpOYl8H1rY/LYK43v7g== +"@commitlint/cli@^19.4.0": + version "19.4.0" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.4.0.tgz#9f93d3ed07e531fcfa371015c8c87e0aa26d974f" + integrity sha512-sJX4J9UioVwZHq7JWM9tjT5bgWYaIN3rC4FP7YwfEwBYiIO+wMyRttRvQLNkow0vCdM0D67r9NEWU0Ui03I4Eg== dependencies: "@commitlint/format" "^19.3.0" "@commitlint/lint" "^19.2.2" - "@commitlint/load" "^19.2.0" - "@commitlint/read" "^19.2.1" + "@commitlint/load" "^19.4.0" + "@commitlint/read" "^19.4.0" "@commitlint/types" "^19.0.3" execa "^8.0.1" yargs "^17.0.0" @@ -1085,10 +1179,10 @@ "@commitlint/rules" "^19.0.3" "@commitlint/types" "^19.0.3" -"@commitlint/load@^19.2.0": - version "19.2.0" - resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-19.2.0.tgz#3ca51fdead4f1e1e09c9c7df343306412b1ef295" - integrity sha512-XvxxLJTKqZojCxaBQ7u92qQLFMMZc4+p9qrIq/9kJDy8DOrEa7P1yx7Tjdc2u2JxIalqT4KOGraVgCE7eCYJyQ== +"@commitlint/load@^19.4.0": + version "19.4.0" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-19.4.0.tgz#7df034e226e300fd577d3f63a72d790d5c821f53" + integrity sha512-I4lCWaEZYQJ1y+Y+gdvbGAx9pYPavqZAZ3/7/8BpWh+QjscAn8AjsUpLV2PycBsEx7gupq5gM4BViV9xwTIJuw== dependencies: "@commitlint/config-validator" "^19.0.3" "@commitlint/execute-rule" "^19.0.0" @@ -1115,10 +1209,10 @@ conventional-changelog-angular "^7.0.0" conventional-commits-parser "^5.0.0" -"@commitlint/read@^19.2.1": - version "19.2.1" - resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-19.2.1.tgz#7296b99c9a989e60e5927fff8388a1dd44299c2f" - integrity sha512-qETc4+PL0EUv7Q36lJbPG+NJiBOGg7SSC7B5BsPWOmei+Dyif80ErfWQ0qXoW9oCh7GTpTNRoaVhiI8RbhuaNw== +"@commitlint/read@^19.4.0": + version "19.4.0" + resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-19.4.0.tgz#3866b1f9a272ef6a388986efa349d24228fc8b00" + integrity sha512-r95jLOEZzKDakXtnQub+zR3xjdnrl2XzerPwm7ch1/cc5JGq04tyaNpa6ty0CRCWdVrk4CZHhqHozb8yZwy2+g== dependencies: "@commitlint/top-level" "^19.0.0" "@commitlint/types" "^19.0.3" @@ -1169,16 +1263,16 @@ "@types/conventional-commits-parser" "^5.0.0" chalk "^5.3.0" -"@cspell/cspell-bundled-dicts@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.8.4.tgz#3ebb5041316dc7c4cfabb3823a6f69dd73ccb31b" - integrity sha512-k9ZMO2kayQFXB3B45b1xXze3MceAMNy9U+D7NTnWB1i3S0y8LhN53U9JWWgqHGPQaHaLHzizL7/w1aGHTA149Q== +"@cspell/cspell-bundled-dicts@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.13.3.tgz#7b214ae6eb6442ef6391765f98c1b9294e3c455e" + integrity sha512-OfCxUBMyayxKyeDaUZG3LQpiyH8MFUbg9nbIZCGh2x8U6N0fHaP9uR6R+gPzdi/bJp32Kr+RC/Yebojd+AQCGA== dependencies: "@cspell/dict-ada" "^4.0.2" - "@cspell/dict-aws" "^4.0.2" + "@cspell/dict-aws" "^4.0.3" "@cspell/dict-bash" "^4.1.3" - "@cspell/dict-companies" "^3.1.2" - "@cspell/dict-cpp" "^5.1.8" + "@cspell/dict-companies" "^3.1.4" + "@cspell/dict-cpp" "^5.1.12" "@cspell/dict-cryptocurrencies" "^5.0.0" "@cspell/dict-csharp" "^4.0.2" "@cspell/dict-css" "^4.0.12" @@ -1187,13 +1281,13 @@ "@cspell/dict-docker" "^1.1.7" "@cspell/dict-dotnet" "^5.0.2" "@cspell/dict-elixir" "^4.0.3" - "@cspell/dict-en-common-misspellings" "^2.0.1" + "@cspell/dict-en-common-misspellings" "^2.0.4" "@cspell/dict-en-gb" "1.1.33" - "@cspell/dict-en_us" "^4.3.21" + "@cspell/dict-en_us" "^4.3.23" "@cspell/dict-filetypes" "^3.0.4" "@cspell/dict-fonts" "^4.0.0" "@cspell/dict-fsharp" "^1.0.1" - "@cspell/dict-fullstack" "^3.1.8" + "@cspell/dict-fullstack" "^3.2.0" "@cspell/dict-gaming-terms" "^1.0.5" "@cspell/dict-git" "^3.0.0" "@cspell/dict-golang" "^6.0.9" @@ -1201,85 +1295,85 @@ "@cspell/dict-haskell" "^4.0.1" "@cspell/dict-html" "^4.0.5" "@cspell/dict-html-symbol-entities" "^4.0.0" - "@cspell/dict-java" "^5.0.6" + "@cspell/dict-java" "^5.0.7" "@cspell/dict-julia" "^1.0.1" - "@cspell/dict-k8s" "^1.0.5" + "@cspell/dict-k8s" "^1.0.6" "@cspell/dict-latex" "^4.0.0" "@cspell/dict-lorem-ipsum" "^4.0.0" "@cspell/dict-lua" "^4.0.3" "@cspell/dict-makefile" "^1.0.0" "@cspell/dict-monkeyc" "^1.0.6" "@cspell/dict-node" "^5.0.1" - "@cspell/dict-npm" "^5.0.16" - "@cspell/dict-php" "^4.0.7" - "@cspell/dict-powershell" "^5.0.4" + "@cspell/dict-npm" "^5.0.18" + "@cspell/dict-php" "^4.0.8" + "@cspell/dict-powershell" "^5.0.5" "@cspell/dict-public-licenses" "^2.0.7" - "@cspell/dict-python" "^4.1.11" + "@cspell/dict-python" "^4.2.4" "@cspell/dict-r" "^2.0.1" "@cspell/dict-ruby" "^5.0.2" - "@cspell/dict-rust" "^4.0.3" - "@cspell/dict-scala" "^5.0.2" - "@cspell/dict-software-terms" "^3.4.1" - "@cspell/dict-sql" "^2.1.3" + "@cspell/dict-rust" "^4.0.5" + "@cspell/dict-scala" "^5.0.3" + "@cspell/dict-software-terms" "^4.0.6" + "@cspell/dict-sql" "^2.1.5" "@cspell/dict-svelte" "^1.0.2" "@cspell/dict-swift" "^2.0.1" "@cspell/dict-terraform" "^1.0.0" - "@cspell/dict-typescript" "^3.1.5" + "@cspell/dict-typescript" "^3.1.6" "@cspell/dict-vue" "^3.0.0" -"@cspell/cspell-json-reporter@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.8.4.tgz#77dfddc021a2f3072bceb877ea1f26ae9893abc3" - integrity sha512-ITpOeNyDHD+4B9QmLJx6YYtrB1saRsrCLluZ34YaICemNLuumVRP1vSjcdoBtefvGugCOn5nPK7igw0r/vdAvA== +"@cspell/cspell-json-reporter@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.13.3.tgz#dbce6f3b1104ba1dca15c645f2a005a228104ef0" + integrity sha512-QrHxWkm0cfD+rTjFOxm5lpE4+wBANDzMIM8NOeQC6v8Dc1L8PUkm6hF6CsEv2tKmuwvdVr+jy6GilDMkPXalCg== dependencies: - "@cspell/cspell-types" "8.8.4" + "@cspell/cspell-types" "8.13.3" -"@cspell/cspell-pipe@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-8.8.4.tgz#ab24c55a4d8eacbb50858fa13259683814504149" - integrity sha512-Uis9iIEcv1zOogXiDVSegm9nzo5NRmsRDsW8CteLRg6PhyZ0nnCY1PZIUy3SbGF0vIcb/M+XsdLSh2wOPqTXww== +"@cspell/cspell-pipe@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-8.13.3.tgz#eca94cd30a97fcea6abffd6189db916505acbce0" + integrity sha512-6a9Zd+fDltgXoJ0fosWqEMx0UdXBXZ7iakhslMNPRmv7GhVAoHBoIXzMVilOE4kYT2Mh/9NM/QW/NbNEpneZIQ== -"@cspell/cspell-resolver@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-resolver/-/cspell-resolver-8.8.4.tgz#73aeb1a25834a4c083b04aa577646305ecf6fdd0" - integrity sha512-eZVw31nSeh6xKl7TzzkZVMTX/mgwhUw40/q1Sqo7CTPurIBg66oelEqKRclX898jzd2/qSK+ZFwBDxvV7QH38A== +"@cspell/cspell-resolver@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-resolver/-/cspell-resolver-8.13.3.tgz#423c96b7834aa33b8f487412e82083652d05406a" + integrity sha512-vlwtMTEWsPPtWfktzT75eGQ0n+0M+9kN+89eSvUUYdCfvY9XAS6z+bTmhS2ULJgntgWtX6gUjABQK0PYYVedOg== dependencies: global-directory "^4.0.1" -"@cspell/cspell-service-bus@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-8.8.4.tgz#bb657b67b79f2676c65e5ee5ac28af149fcb462b" - integrity sha512-KtwJ38uPLrm2Q8osmMIAl2NToA/CMyZCxck4msQJnskdo30IPSdA1Rh0w6zXinmh1eVe0zNEVCeJ2+x23HqW+g== +"@cspell/cspell-service-bus@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-8.13.3.tgz#058121ff78c25afc7bc6218233e6b15e660dbfd5" + integrity sha512-mFkeWXwGQSDxRiN6Kez77GaMNGNgG7T6o9UE42jyXEgf/bLJTpefbUy4fY5pU3p2mA0eoMzmnJX8l+TC5YJpbA== -"@cspell/cspell-types@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-8.8.4.tgz#1fb945f50b776456a437d4bf7438cfa14385d936" - integrity sha512-ya9Jl4+lghx2eUuZNY6pcbbrnResgEAomvglhdbEGqy+B5MPEqY5Jt45APEmGqHzTNks7oFFaiTIbXYJAFBR7A== +"@cspell/cspell-types@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-8.13.3.tgz#180fcebfd1ca21cf09986470de0898b8c7893545" + integrity sha512-lA5GbhLOL6FlKCWNMbooRFgNGfTsM6NJnHz60+EEN7XD9OgpFc7w+MBcK4aHsVCxcrIvnejIc8xQDqPnrdmN3w== "@cspell/dict-ada@^4.0.2": version "4.0.2" resolved "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.0.2.tgz" integrity sha512-0kENOWQeHjUlfyId/aCM/mKXtkEgV0Zu2RhUXCBr4hHo9F9vph+Uu8Ww2b0i5a4ZixoIkudGA+eJvyxrG1jUpA== -"@cspell/dict-aws@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-4.0.2.tgz#6498f1c983c80499054bb31b772aa9562f3aaaed" - integrity sha512-aNGHWSV7dRLTIn8WJemzLoMF62qOaiUQlgnsCwH5fRCD/00gsWCwg106pnbkmK4AyabyxzneOV4dfecDJWkSxw== +"@cspell/dict-aws@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-4.0.3.tgz#7d36d4d5439d1c39b815e0ae19f79e48a823e047" + integrity sha512-0C0RQ4EM29fH0tIYv+EgDQEum0QI6OrmjENC9u98pB8UcnYxGG/SqinuPxo+TgcEuInj0Q73MsBpJ1l5xUnrsw== "@cspell/dict-bash@^4.1.3": version "4.1.3" resolved "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.1.3.tgz" integrity sha512-tOdI3QVJDbQSwPjUkOiQFhYcu2eedmX/PtEpVWg0aFps/r6AyjUQINtTgpqMYnYuq8O1QUIQqnpx21aovcgZCw== -"@cspell/dict-companies@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.1.2.tgz#b335fe5b8847a23673bc4b964ca584339ca669a2" - integrity sha512-OwR5i1xbYuJX7FtHQySmTy3iJtPV1rZQ3jFCxFGwrA1xRQ4rtRcDQ+sTXBCIAoJHkXa84f9J3zsngOKmMGyS/w== +"@cspell/dict-companies@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.1.4.tgz#2e7094416432b8547ec335683f5aac9a49dce47e" + integrity sha512-y9e0amzEK36EiiKx3VAA+SHQJPpf2Qv5cCt5eTUSggpTkiFkCh6gRKQ97rVlrKh5GJrqinDwYIJtTsxuh2vy2Q== -"@cspell/dict-cpp@^5.1.8": - version "5.1.8" - resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.1.8.tgz#c7b2aa1f434f34c46b4849821c9d914dcf2bdad1" - integrity sha512-X5uq0uRqN6cyOZOZV1YKi6g8sBtd0+VoF5NbDWURahGR8TRsiztH0sNqs0IB3X0dW4GakU+n9SXcuEmxynkSsw== +"@cspell/dict-cpp@^5.1.12": + version "5.1.12" + resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.1.12.tgz#52d5ed8b96268e8282f6d7694ee2434b20bafb21" + integrity sha512-6lXLOFIa+k/qBcu0bjaE/Kc6v3sh9VhsDOXD1Dalm3zgd0QIMjp5XBmkpSdCAK3pWCPV0Se7ysVLDfCea1BuXg== "@cspell/dict-cryptocurrencies@^5.0.0": version "5.0.0" @@ -1301,10 +1395,10 @@ resolved "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.0.3.tgz" integrity sha512-cLkwo1KT5CJY5N5RJVHks2genFkNCl/WLfj+0fFjqNR+tk3tBI1LY7ldr9piCtSFSm4x9pO1x6IV3kRUY1lLiw== -"@cspell/dict-data-science@^1.0.11": - version "1.0.11" - resolved "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-1.0.11.tgz" - integrity sha512-TaHAZRVe0Zlcc3C23StZqqbzC0NrodRwoSAc8dis+5qLeLLnOCtagYQeROQvDlcDg3X/VVEO9Whh4W/z4PAmYQ== +"@cspell/dict-data-science@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-data-science/-/dict-data-science-2.0.1.tgz#ef8040821567786d76c6153ac3e4bc265ca65b59" + integrity sha512-xeutkzK0eBe+LFXOFU2kJeAYO6IuFUc1g7iRLr7HeCmlC4rsdGclwGHh61KmttL3+YHQytYStxaRBdGAXWC8Lw== "@cspell/dict-django@^4.1.0": version "4.1.0" @@ -1326,20 +1420,20 @@ resolved "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-4.0.3.tgz" integrity sha512-g+uKLWvOp9IEZvrIvBPTr/oaO6619uH/wyqypqvwpmnmpjcfi8+/hqZH8YNKt15oviK8k4CkINIqNhyndG9d9Q== -"@cspell/dict-en-common-misspellings@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.0.1.tgz#2e472f5128ec38299fc4489638aabdb0d0fb397e" - integrity sha512-uWaP8UG4uvcPyqaG0FzPKCm5kfmhsiiQ45Fs6b3/AEAqfq7Fj1JW0+S3qRt85FQA9SoU6gUJCz9wkK/Ylh7m5A== +"@cspell/dict-en-common-misspellings@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.0.4.tgz#725c5b2c83faff71fcd2183dd04a154c78eed674" + integrity sha512-lvOiRjV/FG4pAGZL3PN2GCVHSTCE92cwhfLGGkOsQtxSmef6WCHfHwp9auafkBlX0yFQSKDfq6/TlpQbjbJBtQ== "@cspell/dict-en-gb@1.1.33": version "1.1.33" resolved "https://registry.npmjs.org/@cspell/dict-en-gb/-/dict-en-gb-1.1.33.tgz" integrity sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g== -"@cspell/dict-en_us@^4.3.21": - version "4.3.21" - resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.21.tgz#a8191e3e04d7ea957cac6575c5c2cf98db8ffa8e" - integrity sha512-Bzoo2aS4Pej/MGIFlATpp0wMt9IzVHrhDjdV7FgkAIXbjrOn67ojbTxCgWs8AuCNVfK8lBYGEvs5+ElH1msF8w== +"@cspell/dict-en_us@^4.3.23": + version "4.3.23" + resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.23.tgz#3362b75a5051405816728ea1bb5ce997582ed383" + integrity sha512-l0SoEQBsi3zDSl3OuL4/apBkxjuj4hLIg/oy6+gZ7LWh03rKdF6VNtSZNXWAmMY+pmb1cGA3ouleTiJIglbsIg== "@cspell/dict-filetypes@^3.0.4": version "3.0.4" @@ -1356,10 +1450,10 @@ resolved "https://registry.npmjs.org/@cspell/dict-fsharp/-/dict-fsharp-1.0.1.tgz" integrity sha512-23xyPcD+j+NnqOjRHgW3IU7Li912SX9wmeefcY0QxukbAxJ/vAN4rBpjSwwYZeQPAn3fxdfdNZs03fg+UM+4yQ== -"@cspell/dict-fullstack@^3.1.8": - version "3.1.8" - resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.1.8.tgz#1bbfa0a165346f6eff9894cf965bf3ce26552797" - integrity sha512-YRlZupL7uqMCtEBK0bDP9BrcPnjDhz7m4GBqCc1EYqfXauHbLmDT8ELha7T/E7wsFKniHSjzwDZzhNXo2lusRQ== +"@cspell/dict-fullstack@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.2.0.tgz#16dd2bd3f03166c8f48600ef032ae1ce184c7b8e" + integrity sha512-sIGQwU6G3rLTo+nx0GKyirR5dQSFeTIzFTOrURw51ISf+jKG9a3OmvsVtc2OANfvEAOLOC9Wfd8WYhmsO8KRDQ== "@cspell/dict-gaming-terms@^1.0.5": version "1.0.5" @@ -1396,20 +1490,20 @@ resolved "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.5.tgz" integrity sha512-p0brEnRybzSSWi8sGbuVEf7jSTDmXPx7XhQUb5bgG6b54uj+Z0Qf0V2n8b/LWwIPJNd1GygaO9l8k3HTCy1h4w== -"@cspell/dict-java@^5.0.6": - version "5.0.6" - resolved "https://registry.npmjs.org/@cspell/dict-java/-/dict-java-5.0.6.tgz" - integrity sha512-kdE4AHHHrixyZ5p6zyms1SLoYpaJarPxrz8Tveo6gddszBVVwIUZ+JkQE1bWNLK740GWzIXdkznpUfw1hP9nXw== +"@cspell/dict-java@^5.0.7": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@cspell/dict-java/-/dict-java-5.0.7.tgz#c0b32d3c208b6419a5eddd010e87196976be2694" + integrity sha512-ejQ9iJXYIq7R09BScU2y5OUGrSqwcD+J5mHFOKbduuQ5s/Eh/duz45KOzykeMLI6KHPVxhBKpUPBWIsfewECpQ== "@cspell/dict-julia@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@cspell/dict-julia/-/dict-julia-1.0.1.tgz#900001417f1c4ea689530adfcc034c848458a0aa" integrity sha512-4JsCLCRhhLMLiaHpmR7zHFjj1qOauzDI5ZzCNQS31TUMfsOo26jAKDfo0jljFAKgw5M2fEG7sKr8IlPpQAYrmQ== -"@cspell/dict-k8s@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.5.tgz#4a4011d9f2f3ab628658573c5f16c0e6dbe30c29" - integrity sha512-Cj+/ZV4S+MKlwfocSJZqe/2UAd/sY8YtlZjbK25VN1nCnrsKrBjfkX29vclwSj1U9aJg4Z9jw/uMjoaKu9ZrpQ== +"@cspell/dict-k8s@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.6.tgz#d46c97136f1504b65dfb6a188005d4ac81d3f461" + integrity sha512-srhVDtwrd799uxMpsPOQqeDJY+gEocgZpoK06EFrb4GRYGhv7lXo9Fb+xQMyQytzOW9dw4DNOEck++nacDuymg== "@cspell/dict-latex@^4.0.0": version "4.0.0" @@ -1441,32 +1535,32 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-node/-/dict-node-5.0.1.tgz#77e17c576a897a3391fce01c1cc5da60bb4c2268" integrity sha512-lax/jGz9h3Dv83v8LHa5G0bf6wm8YVRMzbjJPG/9rp7cAGPtdrga+XANFq+B7bY5+jiSA3zvj10LUFCFjnnCCg== -"@cspell/dict-npm@^5.0.16": - version "5.0.16" - resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.16.tgz#696883918a9876ffd20d5f975bde74a03d27d80e" - integrity sha512-ZWPnLAziEcSCvV0c8k9Qj88pfMu+wZwM5Qks87ShsfBgI8uLZ9tGHravA7gmjH1Gd7Bgxy2ulvXtSqIWPh1lew== +"@cspell/dict-npm@^5.0.18": + version "5.0.18" + resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.18.tgz#7ec5640c97bd25a64de0c9e74eb19dda86fba025" + integrity sha512-weMTyxWpzz19q4wv9n183BtFvdD5fCjtze+bFKpl+4rO/YlPhHL2cXLAeexJz/VDSBecwX4ybTZYoknd1h2J4w== -"@cspell/dict-php@^4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.7.tgz#9eaf8e84529cef681d423402f53ef1eb33cf37b2" - integrity sha512-SUCOBfRDDFz1E2jnAZIIuy8BNbCc8i+VkiL9g4HH9tTN6Nlww5Uz2pMqYS6rZQkXuubqsbkbPlsRiuseEnTmYA== +"@cspell/dict-php@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.8.tgz#fedce3109dff13a0f3d8d88ba604d6edd2b9fb70" + integrity sha512-TBw3won4MCBQ2wdu7kvgOCR3dY2Tb+LJHgDUpuquy3WnzGiSDJ4AVelrZdE1xu7mjFJUr4q48aB21YT5uQqPZA== -"@cspell/dict-powershell@^5.0.4": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.4.tgz#db2bc6a86700a2f829dc1b3b04f6cb3a916fd928" - integrity sha512-eosDShapDgBWN9ULF7+sRNdUtzRnUdsfEdBSchDm8FZA4HOqxUSZy3b/cX/Rdw0Fnw0AKgk0kzgXw7tS6vwJMQ== +"@cspell/dict-powershell@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.5.tgz#3319d2fbad740e164a78386d711668bfe335c1f2" + integrity sha512-3JVyvMoDJesAATYGOxcUWPbQPUvpZmkinV3m8HL1w1RrjeMVXXuK7U1jhopSneBtLhkU+9HKFwgh9l9xL9mY2Q== "@cspell/dict-public-licenses@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.7.tgz#ccd67a91a6bd5ed4b5117c2f34e9361accebfcb7" integrity sha512-KlBXuGcN3LE7tQi/GEqKiDewWGGuopiAD0zRK1QilOx5Co8XAvs044gk4MNIQftc8r0nHeUI+irJKLGcR36DIQ== -"@cspell/dict-python@^4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.1.11.tgz" - integrity sha512-XG+v3PumfzUW38huSbfT15Vqt3ihNb462ulfXifpQllPok5OWynhszCLCRQjQReV+dgz784ST4ggRxW452/kVg== +"@cspell/dict-python@^4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-python/-/dict-python-4.2.4.tgz#add81749939c6f123dbd0662385153506be951e0" + integrity sha512-sCtLBqMreb+8zRW2bXvFsfSnRUVU6IFm4mT6Dc4xbz0YajprbaPPh/kOUTw5IJRP8Uh+FFb7Xp2iH03CNWRq/A== dependencies: - "@cspell/dict-data-science" "^1.0.11" + "@cspell/dict-data-science" "^2.0.1" "@cspell/dict-r@^2.0.1": version "2.0.1" @@ -1478,25 +1572,25 @@ resolved "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.0.2.tgz" integrity sha512-cIh8KTjpldzFzKGgrqUX4bFyav5lC52hXDKo4LbRuMVncs3zg4hcSf4HtURY+f2AfEZzN6ZKzXafQpThq3dl2g== -"@cspell/dict-rust@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.3.tgz#ad61939f78bd63a07ae885f429eab24a74ad7f5e" - integrity sha512-8DFCzkFQ+2k3fDaezWc/D+0AyiBBiOGYfSDUfrTNU7wpvUvJ6cRcAUshMI/cn2QW/mmxTspRgVlXsE6GUMz00Q== +"@cspell/dict-rust@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.5.tgz#41f3e26fdd3d121c3a24c122d4a703abbb48c4c3" + integrity sha512-DIvlPRDemjKQy8rCqftAgGNZxY5Bg+Ps7qAIJjxkSjmMETyDgl0KTVuaJPt7EK4jJt6uCZ4ILy96npsHDPwoXA== -"@cspell/dict-scala@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.2.tgz#d732ab24610cc9f6916fb8148f6ef5bdd945fc47" - integrity sha512-v97ClgidZt99JUm7OjhQugDHmhx4U8fcgunHvD/BsXWjXNj4cTr0m0YjofyZoL44WpICsNuFV9F/sv9OM5HUEw== +"@cspell/dict-scala@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.3.tgz#85a469b2d139766b6307befc89243928e3d82b39" + integrity sha512-4yGb4AInT99rqprxVNT9TYb1YSpq58Owzq7zi3ZS5T0u899Y4VsxsBiOgHnQ/4W+ygi+sp+oqef8w8nABR2lkg== -"@cspell/dict-software-terms@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-3.4.1.tgz#1929d1afe79c303092b37db977342bb5c6903481" - integrity sha512-JgNHVdWEUhZKCYBiAjsLojkw8WhvsTXyk/IfFby0Lzbl+/AoJvL/XZqr0pvqfpBjbv7pwtnjahrbGxPRCOV+gA== +"@cspell/dict-software-terms@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-4.0.6.tgz#df1efb809a50ae4292f85a90d1481dafcc13b414" + integrity sha512-UDhUzNSf7GN529a0Ip9hlSoGbpscz0YlUYBEJmZBXi8otpkrbCJqs50T74Ppd+SWqNil04De8urv4af2c6SY5Q== -"@cspell/dict-sql@^2.1.3": - version "2.1.3" - resolved "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.1.3.tgz" - integrity sha512-SEyTNKJrjqD6PAzZ9WpdSu6P7wgdNtGV2RV8Kpuw1x6bV+YsSptuClYG+JSdRExBTE6LwIe1bTklejUp3ZP8TQ== +"@cspell/dict-sql@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-sql/-/dict-sql-2.1.5.tgz#068c7a8840d75418fd46a0b062c0ed2d5742f2b8" + integrity sha512-FmxanytHXss7GAWAXmgaxl3icTCW7YxlimyOSPNfm+njqeUDjw3kEv4mFNDDObBJv8Ec5AWCbUDkWIpkE3IpKg== "@cspell/dict-svelte@^1.0.2": version "1.0.2" @@ -1513,27 +1607,32 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-terraform/-/dict-terraform-1.0.0.tgz#c7b073bb3a03683f64cc70ccaa55ce9742c46086" integrity sha512-Ak+vy4HP/bOgzf06BAMC30+ZvL9mzv21xLM2XtfnBLTDJGdxlk/nK0U6QT8VfFLqJ0ZZSpyOxGsUebWDCTr/zQ== -"@cspell/dict-typescript@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.5.tgz#15bd74651fb2cf0eff1150f07afee9543206bfab" - integrity sha512-EkIwwNV/xqEoBPJml2S16RXj65h1kvly8dfDLgXerrKw6puybZdvAHerAph6/uPTYdtLcsPyJYkPt5ISOJYrtw== +"@cspell/dict-typescript@^3.1.6": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.6.tgz#2d5351786787bf3609da65ba17d9bc345995a36d" + integrity sha512-1beC6O4P/j23VuxX+i0+F7XqPVc3hhiAzGJHEKqnWf5cWAXQtg0xz3xQJ5MvYx2a7iLaSa+lu7+05vG9UHyu9Q== "@cspell/dict-vue@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@cspell/dict-vue/-/dict-vue-3.0.0.tgz" integrity sha512-niiEMPWPV9IeRBRzZ0TBZmNnkK3olkOPYxC1Ny2AX4TGlYRajcW0WUtoSHmvvjZNfWLSg2L6ruiBeuPSbjnG6A== -"@cspell/dynamic-import@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-8.8.4.tgz#895b30da156daa7dde9c153ea9ca7c707541edbf" - integrity sha512-tseSxrybznkmsmPaAB4aoHB9wr8Q2fOMIy3dm+yQv+U1xj+JHTN9OnUvy9sKiq0p3DQGWm/VylgSgsYaXrEHKQ== +"@cspell/dynamic-import@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-8.13.3.tgz#6809ae3c5f46766145ae7f615889a1e1686d59ac" + integrity sha512-YN83CFWnMkt9B0q0RBadfEoptUaDRqBikh8b91MOQ0haEnUo6t57j4jAaLnbIEP4ynzMhgruWFKpIC/QaEtCuA== dependencies: import-meta-resolve "^4.1.0" -"@cspell/strong-weak-map@8.8.4": - version "8.8.4" - resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-8.8.4.tgz#1040b09b5fcbd81eba0430d98580b3caf0825b2a" - integrity sha512-gticEJGR6yyGeLjf+mJ0jZotWYRLVQ+J0v1VpsR1nKnXTRJY15BWXgEA/ifbU/+clpyCek79NiCIXCvmP1WT4A== +"@cspell/strong-weak-map@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-8.13.3.tgz#069f5e95f70d884415feec1bf53a1c20854b33f9" + integrity sha512-/QYUEthesPuDarOHa6kcWKJmVq0HIotjPrmAWQ5QpH+dDik1Qin4G/9QdnWX75ueR4DC4WFjBNBU14C4TVSwHQ== + +"@cspell/url@8.13.3": + version "8.13.3" + resolved "https://registry.yarnpkg.com/@cspell/url/-/url-8.13.3.tgz#2c400e581570fe2fb95197a2e6c64ceeeb6ceb8b" + integrity sha512-hsxoTnZHwtdR2x9QEE6yfDBB1LUwAj67o1GyKTvI8A2OE/AfzAttirZs+9sxgOGWoBdTOxM9sMLtqB3SxtDB3A== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -1561,19 +1660,19 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint-community/regexpp@^4.6.1": - version "4.6.2" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz" - integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== +"@eslint-community/regexpp@^4.11.0": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== -"@eslint/config-array@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.16.0.tgz#bb3364fc39ee84ec3a62abdc4b8d988d99dfd706" - integrity sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg== +"@eslint/config-array@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.1.tgz#d9b8b8b6b946f47388f32bedfd3adf29ca8f8910" + integrity sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA== dependencies: "@eslint/object-schema" "^2.1.4" debug "^4.3.1" - minimatch "^3.0.5" + minimatch "^3.1.2" "@eslint/eslintrc@^3.1.0": version "3.1.0" @@ -1590,10 +1689,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.5.0", "@eslint/js@^9.5.0": - version "9.5.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.5.0.tgz#0e9c24a670b8a5c86bff97b40be13d8d8f238045" - integrity sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w== +"@eslint/js@9.9.0", "@eslint/js@^9.9.0": + version "9.9.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.0.tgz#d8437adda50b3ed4401964517b64b4f59b0e2638" + integrity sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug== "@eslint/object-schema@^2.1.4": version "2.1.4" @@ -1605,6 +1704,11 @@ resolved "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz" integrity sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg== +"@golevelup/ts-jest@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@golevelup/ts-jest/-/ts-jest-0.5.2.tgz#39ebe5d0f50b7838b4985aebccecc737832aa1a2" + integrity sha512-4uJl1RYETwBoKrxVxA+BOrkS0ex4W+XnUKvfccSZpt2sLS6GyYrVl9x+C3tFd6tne7bG34ppzrWd2AT6+D5IqA== + "@graphql-tools/merge@9.0.0": version "9.0.0" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.0.tgz" @@ -1666,6 +1770,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -1941,6 +2050,15 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" @@ -1956,6 +2074,11 @@ resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.3": version "0.3.5" resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz" @@ -2006,22 +2129,30 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@ljharb/through@^2.3.11": - version "2.3.12" - resolved "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz" - integrity sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: - call-bind "^1.0.5" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@ljharb/through@^2.3.12": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.13.tgz#b7e4766e0b65aa82e529be945ab078de79874edc" + integrity sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ== + dependencies: + call-bind "^1.0.7" "@lukeed/csprng@^1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz" integrity sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g== -"@microsoft/tsdoc@^0.14.2": - version "0.14.2" - resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz" - integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== +"@microsoft/tsdoc@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz#f29a55df17cb6e87cfbabce33ff6a14a9f85076d" + integrity sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA== "@mongodb-js/saslprep@^1.1.5": version "1.1.5" @@ -2030,69 +2161,115 @@ dependencies: sparse-bitfield "^3.0.3" +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11" + integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855" + integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb" + integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159" + integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3" + integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" + integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== + "@nestjs/axios@^3.0.2": version "3.0.2" resolved "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz" integrity sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ== -"@nestjs/cli@^10.3.2": - version "10.3.2" - resolved "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz" - integrity sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg== +"@nestjs/bull-shared@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz#a94a756424429e1c50662633805ced1cacd21f2f" + integrity sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig== dependencies: - "@angular-devkit/core" "17.1.2" - "@angular-devkit/schematics" "17.1.2" - "@angular-devkit/schematics-cli" "17.1.2" + tslib "2.6.3" + +"@nestjs/bullmq@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/bullmq/-/bullmq-10.2.0.tgz#f548a191bcbb6febcc3a50a6aaa7d81db09516c3" + integrity sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q== + dependencies: + "@nestjs/bull-shared" "^10.2.0" + tslib "2.6.3" + +"@nestjs/cache-manager@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz#4b0e7c4112c7b8c2a869d64f998aaf8a1bf0040d" + integrity sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA== + +"@nestjs/cli@^10.4.4": + version "10.4.4" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.4.tgz#6fed6fa3259417ea3dc0b62a3888e4aedbaa1642" + integrity sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw== + dependencies: + "@angular-devkit/core" "17.3.8" + "@angular-devkit/schematics" "17.3.8" + "@angular-devkit/schematics-cli" "17.3.8" "@nestjs/schematics" "^10.0.1" chalk "4.1.2" chokidar "3.6.0" - cli-table3 "0.6.3" + cli-table3 "0.6.5" commander "4.1.1" fork-ts-checker-webpack-plugin "9.0.2" - glob "10.3.10" + glob "10.4.2" inquirer "8.2.6" node-emoji "1.11.0" ora "5.4.1" - rimraf "4.4.1" - shelljs "0.8.5" - source-map-support "0.5.21" tree-kill "1.2.2" tsconfig-paths "4.2.0" tsconfig-paths-webpack-plugin "4.1.0" typescript "5.3.3" - webpack "5.90.1" + webpack "5.93.0" webpack-node-externals "3.0.0" -"@nestjs/common@^10.3.9": - version "10.3.9" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.9.tgz#4e85d8fa6ae201a1c5f49d4d09b205bc3672ed1f" - integrity sha512-JAQONPagMa+sy/fcIqh/Hn3rkYQ9pQM51vXCFNOM5ujefxUVqn3gwFRMN8Y1+MxdUHipV+8daEj2jEm0IqJzOA== +"@nestjs/common@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.0.tgz#f0507aa6d079b696a090e233780eb764c64f1c71" + integrity sha512-cGQJBMypG1qf0h31dvIYSffr/8+JhFd7qScJ4mqgF5HKT69WveW14zQcxavXzXI/LOE4vUvCu3QBeqcRBIs/9A== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.2" + tslib "2.6.3" -"@nestjs/config@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.2.2.tgz#7e895edb2878564e0a7fc411614902b0330ca940" - integrity sha512-vGICPOui5vE6kPz1iwQ7oCnp3qWgqxldPmBQ9onkVoKlBtyc83KJCr7CjuVtf4OdovMAVcux1d8Q6jglU2ZphA== +"@nestjs/config@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.2.3.tgz#569888a33ada50b0f182002015e152e054990016" + integrity sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w== dependencies: dotenv "16.4.5" dotenv-expand "10.0.0" lodash "4.17.21" - uuid "9.0.1" -"@nestjs/core@^10.3.9": - version "10.3.9" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.9.tgz#fe3645eb4974423de5503a19d08f85a67693e72f" - integrity sha512-NzZUfWAmaf8sqhhwoRA+CuqxQe+P4Rz8PZp5U7CdCbjyeB9ZVGcBkihcJC9wMdtiOWHRndB2J8zRfs5w06jK3w== +"@nestjs/core@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.0.tgz#f7b6e359c105ec54a861962f6085d494b72488cc" + integrity sha512-vNVJ0H8n3FyIxrFibgV2tRbjKsVm90u//kinE0m7s6ygv+KhnGMrQvWGX0kk9wbsZwRMW5JMpnBWDUS4wu4yPg== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" path-to-regexp "3.2.0" - tslib "2.6.2" + tslib "2.6.3" "@nestjs/graphql@~12.0.11": version "12.0.11" @@ -2132,34 +2309,26 @@ resolved "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz" integrity sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg== -"@nestjs/mongoose@^10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@nestjs/mongoose/-/mongoose-10.0.6.tgz#6081975eaa658a6097b63d139a82f58b2b35956c" - integrity sha512-J8jFgSvCDEKMXU57QAIdXIlWQsOFWK+x0PM1KI/0zHe3/4JrAtFGTFD08hRX3IHk+WJT9g/XQIpMSNM7/10Jlg== +"@nestjs/mongoose@^10.0.10": + version "10.0.10" + resolved "https://registry.yarnpkg.com/@nestjs/mongoose/-/mongoose-10.0.10.tgz#1206df06b6f744f62b0bd7502074c13f17177739" + integrity sha512-3Ff60ock8nwlAJC823TG91Qy+Qc6av+ddIb6n6wlFsTK0akDF/aTcagX8cF8uI8mWxCWjEwEsgv99vo6p0yJ+w== "@nestjs/passport@^10.0.3": version "10.0.3" resolved "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz" integrity sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ== -"@nestjs/platform-express@^10.3.9": - version "10.3.9" - resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.3.9.tgz#769c6027f8b3d1e144218403762710f96a174821" - integrity sha512-si/UzobP6YUtYtCT1cSyQYHHzU3yseqYT6l7OHSMVvfG1+TqxaAqI6nmrix02LO+l1YntHRXEs3p+v9a7EfrSQ== +"@nestjs/platform-express@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.0.tgz#d08f1df6790a5a108676cb3bdd0a991388289276" + integrity sha512-DxrNsqywNVRs+4tmEXKNotumXEEGw+EvG2f9MyvDnHYU7tCZAT9ZsVnT6waM3lrjSmyjMaae8JuiMI8bnZj44g== dependencies: body-parser "1.20.2" cors "2.8.5" express "4.19.2" multer "1.4.4-lts.1" - tslib "2.6.2" - -"@nestjs/schedule@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-4.0.2.tgz#573061b152e174f1bf590f5ec00a2c400e8bbee8" - integrity sha512-po9oauE7fO0CjhDKvVC2tzEgjOUwhxYoIsXIVkgfu+xaDMmzzpmXY2s1LT4oP90Z+PaTtPoAHmhslnYmo4mSZg== - dependencies: - cron "3.1.7" - uuid "9.0.1" + tslib "2.6.3" "@nestjs/schematics@^10.0.1": version "10.0.1" @@ -2172,28 +2341,28 @@ jsonc-parser "3.2.0" pluralize "8.0.0" -"@nestjs/schematics@^10.1.1": - version "10.1.1" - resolved "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz" - integrity sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig== +"@nestjs/schematics@^10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.1.3.tgz#8bd80ab9fab6a02586524bd2c545b0ea787cf62c" + integrity sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg== dependencies: - "@angular-devkit/core" "17.1.2" - "@angular-devkit/schematics" "17.1.2" + "@angular-devkit/core" "17.3.8" + "@angular-devkit/schematics" "17.3.8" comment-json "4.2.3" - jsonc-parser "3.2.1" + jsonc-parser "3.3.1" pluralize "8.0.0" -"@nestjs/swagger@^7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-7.3.1.tgz#353fdd5bd6f23564505117b1c82d7decc145e8fe" - integrity sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw== +"@nestjs/swagger@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-7.4.0.tgz#e61dbefdfc1d4011327a256896953c74e511c850" + integrity sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g== dependencies: - "@microsoft/tsdoc" "^0.14.2" + "@microsoft/tsdoc" "^0.15.0" "@nestjs/mapped-types" "2.0.5" js-yaml "4.1.0" lodash "4.17.21" path-to-regexp "3.2.0" - swagger-ui-dist "5.11.2" + swagger-ui-dist "5.17.14" "@nestjs/terminus@^10.2.3": version "10.2.3" @@ -2203,17 +2372,17 @@ boxen "5.1.2" check-disk-space "3.4.0" -"@nestjs/testing@^10.3.9": - version "10.3.9" - resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.3.9.tgz#27fb0e23b129147f8de100ac40645ebf6a865c3a" - integrity sha512-z24SdpZIRtYyM5s2vnu7rbBosXJY/KcAP7oJlwgFa/h/z/wg8gzyoKy5lhibH//OZNO+pYKajV5wczxuy5WeAg== +"@nestjs/testing@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.4.0.tgz#9902dbe557acfd460973ced4b380591d8c470fbf" + integrity sha512-oAQe3Yb4/JlHtsBcKmueEvPZDoONp7LsNwGnMAeyhoBLuPBXDhZnNgMY2UtT4FfNmudBQBKR/vq/fOQRax/4Hg== dependencies: - tslib "2.6.2" + tslib "2.6.3" -"@nestjs/throttler@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-5.2.0.tgz#9a70f1e13b3015977ed0fe740e23e3c5cba2d49a" - integrity sha512-G/G/MV3xf6sy1DwmnJsgeL+d2tQ/xGRNa9ZhZjm9Kyxp+3+ylGzwJtcnhWlN82PMEp3TiDQpTt+9waOIg/bpPg== +"@nestjs/throttler@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-6.1.0.tgz#ce78f5d24078862809e94a55015e74285df2f53a" + integrity sha512-MzwameXplM8FhQiN79U2zEIZQNEM0BM9kbOigriFSzdNCEfyIoNKHhGFMfsTvMxP119Ydi0Zvkt3W2CQUSIH5Q== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -2252,13 +2421,6 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@opentelemetry/api-logs@0.51.1": - version "0.51.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.51.1.tgz#ded1874c04516c2b8cb24828eef3d6c3d1f75343" - integrity sha512-E3skn949Pk1z2XtXu/lxf6QAZpawuTM/IUEXcAzpiUkTd73Hmvw26FiN3cJuTmkpM5hZzHwkomVdtrh/n/zzwA== - dependencies: - "@opentelemetry/api" "^1.0.0" - "@opentelemetry/api-logs@0.52.0": version "0.52.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz#b117c1fc6fc457249739bbe21571cefc55e5092c" @@ -2266,7 +2428,14 @@ dependencies: "@opentelemetry/api" "^1.0.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@^1.8": +"@opentelemetry/api-logs@0.52.1": + version "0.52.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" + integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A== + dependencies: + "@opentelemetry/api" "^1.0.0" + +"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.8": version "1.8.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== @@ -2276,10 +2445,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== -"@opentelemetry/context-async-hooks@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.0.tgz#bc3dcb1302b34b0f56047dd0d0f56b33013f657f" - integrity sha512-sBW313mnMyFg0cp/40BRzrZBWG+581s2j5gIsa5fgGadswyILk4mNFATsqrCOpAx945RDuZ2B7ThQLgor9OpfA== +"@opentelemetry/context-async-hooks@^1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz#810bff2fcab84ec51f4684aff2d21f6c057d9e73" + integrity sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ== "@opentelemetry/core@1.24.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.8.0": version "1.24.1" @@ -2288,135 +2457,133 @@ dependencies: "@opentelemetry/semantic-conventions" "1.24.1" -"@opentelemetry/core@1.25.0", "@opentelemetry/core@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.0.tgz#ad034f5c2669f589bd703bfbbaa38b51f8504053" - integrity sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ== +"@opentelemetry/core@1.25.1", "@opentelemetry/core@^1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.1.tgz#ff667d939d128adfc7c793edae2f6bca177f829d" + integrity sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ== dependencies: - "@opentelemetry/semantic-conventions" "1.25.0" + "@opentelemetry/semantic-conventions" "1.25.1" -"@opentelemetry/instrumentation-connect@0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.37.0.tgz#ab1bc3d33058bfc647d4b158295b589d11d619df" - integrity sha512-SeQktDIH5rNzjiEiazWiJAIXkmnLOnNV7wwHpahrqE0Ph+Z3heqMfxRtoMtbdJSIYLfcNZYO51AjxZ00IXufdw== +"@opentelemetry/instrumentation-connect@0.38.0": + version "0.38.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz#1f4aa27894eac2538fb3c8fce7b1be92cae0217e" + integrity sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" "@types/connect" "3.4.36" -"@opentelemetry/instrumentation-express@0.40.1": - version "0.40.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.40.1.tgz#b4c31a352691b060b330e4c028a8ef5472b89e27" - integrity sha512-+RKMvVe2zw3kIXRup9c1jFu3T4d0fs5aKy015TpiMyoCKX1UMu3Z0lfgYtuyiSTANvg5hZnDbWmQmqSPj9VTvg== +"@opentelemetry/instrumentation-express@0.41.1": + version "0.41.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz#658561df6ffbae86f5ad33e8d7ef2abb7b4967fc" + integrity sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-fastify@0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.37.0.tgz#c9537050d222d89ad4c3930b7b21a58016206f6d" - integrity sha512-WRjwzNZgupSzbEYvo9s+QuHJRqZJjVdNxSEpGBwWK8RKLlHGwGVAu0gcc2gPamJWUJsGqPGvahAPWM18ZkWj6A== +"@opentelemetry/instrumentation-fastify@0.38.0": + version "0.38.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz#0cb02ee1156197075e8a90e4fd18a6b6c94221ba" + integrity sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-graphql@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.41.0.tgz#b3f1c7e0bb18400b1336f781f209f6b73608bd89" - integrity sha512-R/gXeljgIhaRDKquVkKYT5QHPnFouM8ooyePZEP0kqyaVAedtR1V7NfAUJbxfTG5fBQa5wdmLjvu63+tzRXZCA== +"@opentelemetry/instrumentation-graphql@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz#588a18c39e3b3f655bc09243566172ab0b638d35" + integrity sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q== dependencies: "@opentelemetry/instrumentation" "^0.52.0" -"@opentelemetry/instrumentation-hapi@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.39.0.tgz#c6a43440baac714aba57d12ee363b72a02378eed" - integrity sha512-ik2nA9Yj2s2ay+aNY+tJsKCsEx6Tsc2g/MK0iWBW5tibwrWKTy1pdVt5sB3kd5Gkimqj23UV5+FH2JFcQLeKug== +"@opentelemetry/instrumentation-hapi@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz#ae11190f0f57cdb4dc8d792cb8bca61e5343684c" + integrity sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-http@0.52.0": - version "0.52.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz#a2fd280a493591d2cf4db534253ca406580569f7" - integrity sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA== +"@opentelemetry/instrumentation-http@0.52.1": + version "0.52.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz#12061501601838d1c912f9c29bdd40a13a7e44cf" + integrity sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q== dependencies: - "@opentelemetry/core" "1.25.0" - "@opentelemetry/instrumentation" "0.52.0" - "@opentelemetry/semantic-conventions" "1.25.0" + "@opentelemetry/core" "1.25.1" + "@opentelemetry/instrumentation" "0.52.1" + "@opentelemetry/semantic-conventions" "1.25.1" semver "^7.5.2" -"@opentelemetry/instrumentation-ioredis@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.41.0.tgz#41b60babdce893df7466b13a8896a71c81a80813" - integrity sha512-rxiLloU8VyeJGm5j2fZS8ShVdB82n7VNP8wTwfUQqDwRfHCnkzGr+buKoxuhGD91gtwJ91RHkjHA1Eg6RqsUTg== +"@opentelemetry/instrumentation-ioredis@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz#0f488ffc68af3caa474e2f67861759075170729c" + integrity sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.23.0" -"@opentelemetry/instrumentation-koa@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.41.0.tgz#31d75ebc4c53c9c902f7ef3f73e52d575fce9628" - integrity sha512-mbPnDt7ELvpM2S0vixYUsde7122lgegLOJQxx8iJQbB8YHal/xnTh9v7IfArSVzIDo+E+080hxZyUZD4boOWkw== +"@opentelemetry/instrumentation-koa@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz#1c180f3605448c2e57a4ba073b69ffba7b2970b3" + integrity sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" - "@types/koa" "2.14.0" - "@types/koa__router" "12.0.3" -"@opentelemetry/instrumentation-mongodb@0.45.0": - version "0.45.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.45.0.tgz#d6373e30f3e83eba87f7e6e2ea72c1351467d6b5" - integrity sha512-xnZP9+ayeB1JJyNE9cIiwhOJTzNEsRhXVdLgfzmrs48Chhhk026mQdM5CITfyXSCfN73FGAIB8d91+pflJEfWQ== +"@opentelemetry/instrumentation-mongodb@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz#e3720e8ca3ca9f228fbf02f0812f7518c030b05e" + integrity sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/sdk-metrics" "^1.9.1" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-mongoose@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.39.0.tgz#2d5070bb0838769b8dd099b6402f42e1269f527a" - integrity sha512-J1r66A7zJklPPhMtrFOO7/Ud2p0Pv5u8+r23Cd1JUH6fYPmftNJVsLp2urAt6PHK4jVqpP/YegN8wzjJ2mZNPQ== +"@opentelemetry/instrumentation-mongoose@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.40.0.tgz#9c888312e524c381bfdf56a094c799150332dd51" + integrity sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-mysql2@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.39.0.tgz#1719441f58e3f3418c2c3a7b15b48c187d8e3f90" - integrity sha512-Iypuq2z6TCfriAXCIZjRq8GTFCKhQv5SpXbmI+e60rYdXw8NHtMH4NXcGF0eKTuoCsC59IYSTUvDQYDKReaszA== +"@opentelemetry/instrumentation-mysql2@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz#fa2992c36d54427dccea68e5c69fff01103dabe6" + integrity sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" "@opentelemetry/sql-common" "^0.40.1" -"@opentelemetry/instrumentation-mysql@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.39.0.tgz#b55afe5b1249363f42c6092529466b057297ab94" - integrity sha512-8snHPh83rhrDf31v9Kq0Nf+ts8hdr7NguuszRqZomZBHgE0+UyXZSkXHAAFZoBPPRMGyM68uaFE5hVtFl+wOcA== +"@opentelemetry/instrumentation-mysql@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz#bde5894c8eb447a4b8e940b030b2b73898da03fa" + integrity sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" "@types/mysql" "2.15.22" -"@opentelemetry/instrumentation-nestjs-core@0.38.0": - version "0.38.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.38.0.tgz#d4296936723f1dfbd11747a84a87d17a3da0bc74" - integrity sha512-M381Df1dM8aqihZz2yK+ugvMFK5vlHG/835dc67Sx2hH4pQEQYDA2PpFPTgc9AYYOydQaj7ClFQunESimjXDgg== +"@opentelemetry/instrumentation-nestjs-core@0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz#733fef4306c796951d7ea1951b45f9df0aed234d" + integrity sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.23.0" -"@opentelemetry/instrumentation-pg@0.42.0": - version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.42.0.tgz#a73de6c057b4a8b99c964d2bbf2fdad304284be9" - integrity sha512-sjgcM8CswYy8zxHgXv4RAZ09DlYhQ+9TdlourUs63Df/ek5RrB1ZbjznqW7PB6c3TyJJmX6AVtPTjAsROovEjA== +"@opentelemetry/instrumentation-pg@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz#3cd94ad5144e1fd326a921280fa8bb7b49005eb5" + integrity sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/semantic-conventions" "^1.22.0" @@ -2424,46 +2591,46 @@ "@types/pg" "8.6.1" "@types/pg-pool" "2.0.4" -"@opentelemetry/instrumentation-redis-4@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.40.0.tgz#4a1bc9bebfb869de8d982b1a1a5b550bdb68d15b" - integrity sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A== +"@opentelemetry/instrumentation-redis-4@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.0.tgz#6c1b1a37c18478887f346a3bc7ef309ee9f726c0" + integrity sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg== dependencies: "@opentelemetry/instrumentation" "^0.52.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation@0.52.0", "@opentelemetry/instrumentation@^0.52.0": - version "0.52.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz#f8b790bfb1c61c27e0ba846bc6d0e377da195d1e" - integrity sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ== +"@opentelemetry/instrumentation@0.52.1", "@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.1": + version "0.52.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" + integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== dependencies: - "@opentelemetry/api-logs" "0.52.0" + "@opentelemetry/api-logs" "0.52.1" "@types/shimmer" "^1.0.2" - import-in-the-middle "1.8.0" + import-in-the-middle "^1.8.1" require-in-the-middle "^7.1.1" semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" - integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== +"@opentelemetry/instrumentation@^0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e" + integrity sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw== dependencies: "@types/shimmer" "^1.0.2" - import-in-the-middle "1.4.2" + import-in-the-middle "1.7.1" require-in-the-middle "^7.1.1" semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51": - version "0.51.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.51.1.tgz#46fb2291150ec6923e50b2f094b9407bc726ca9b" - integrity sha512-JIrvhpgqY6437QIqToyozrUG1h5UhwHkaGK/WAX+fkrpyPtc+RO5FkRtUd9BH0MibabHHvqsnBGKfKVijbmp8w== +"@opentelemetry/instrumentation@^0.52.0": + version "0.52.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz#f8b790bfb1c61c27e0ba846bc6d0e377da195d1e" + integrity sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ== dependencies: - "@opentelemetry/api-logs" "0.51.1" + "@opentelemetry/api-logs" "0.52.0" "@types/shimmer" "^1.0.2" - import-in-the-middle "1.7.4" + import-in-the-middle "1.8.0" require-in-the-middle "^7.1.1" semver "^7.5.2" shimmer "^1.2.1" @@ -2481,13 +2648,13 @@ "@opentelemetry/core" "1.24.1" "@opentelemetry/semantic-conventions" "1.24.1" -"@opentelemetry/resources@1.25.0", "@opentelemetry/resources@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.0.tgz#84a1e70097e342aa2047aac97be114ad14966793" - integrity sha512-iHjydPMYJ+Li1auveJCq2rp5U2h6Mhq8BidiyE0jfVlDTFyR1ny8AfJHfmFzJ/RAM8vT8L7T21kcmGybxZC7lQ== +"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.1.tgz#bb9a674af25a1a6c30840b755bc69da2796fefbb" + integrity sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ== dependencies: - "@opentelemetry/core" "1.25.0" - "@opentelemetry/semantic-conventions" "1.25.0" + "@opentelemetry/core" "1.25.1" + "@opentelemetry/semantic-conventions" "1.25.1" "@opentelemetry/sdk-metrics@^1.9.1": version "1.24.1" @@ -2507,24 +2674,24 @@ "@opentelemetry/resources" "1.24.1" "@opentelemetry/semantic-conventions" "1.24.1" -"@opentelemetry/sdk-trace-base@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.0.tgz#263f9ce19001c5cd7a814d0eb40ebc6469ae763d" - integrity sha512-6+g2fiRQUG39guCsKVeY8ToeuUf3YUnPkN6DXRA1qDmFLprlLvZm9cS6+chgbW70cZJ406FTtSCDnJwxDC5sGQ== +"@opentelemetry/sdk-trace-base@^1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz#cbc1e60af255655d2020aa14cde17b37bd13df37" + integrity sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw== dependencies: - "@opentelemetry/core" "1.25.0" - "@opentelemetry/resources" "1.25.0" - "@opentelemetry/semantic-conventions" "1.25.0" + "@opentelemetry/core" "1.25.1" + "@opentelemetry/resources" "1.25.1" + "@opentelemetry/semantic-conventions" "1.25.1" "@opentelemetry/semantic-conventions@1.24.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0": version "1.24.1" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.24.1.tgz#d4bcebda1cb5146d47a2a53daaa7922f8e084dfb" integrity sha512-VkliWlS4/+GHLLW7J/rVBA00uXus1SWvwFvcUDxDwmFxYfg/2VI6ekwdXS28cjI8Qz2ky2BzG8OUHo+WeYIWqw== -"@opentelemetry/semantic-conventions@1.25.0", "@opentelemetry/semantic-conventions@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz#390eb4d42a29c66bdc30066af9035645e9bb7270" - integrity sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ== +"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e" + integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" @@ -2538,78 +2705,113 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/instrumentation@5.15.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.15.0.tgz#9ec061b35761579ffa896bdf19c6a0bf53247593" - integrity sha512-fCWOOOajTKOUEp43gRmBqwt6oN9bPJcLiloi2OG/2ED0N5z62Cuza6FDrlm3SJHQAXYlXqLE0HLdEE5WcUkOzg== +"@prisma/instrumentation@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.17.0.tgz#f741ff517f54b1a896fb8605e0d702f29855c6cb" + integrity sha512-c1Sle4ji8aasMcYfBBHFM56We4ljfenVtRmS8aY06BllS7SoU6SmJBwG7vil+GHiR0Yrh+t9iBwt4AY0Jr4KNQ== dependencies: "@opentelemetry/api" "^1.8" - "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51" + "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0" "@opentelemetry/sdk-trace-base" "^1.22" -"@sentry/core@8.9.2": - version "8.9.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.9.2.tgz#af0f2ec25b88da5467cf327d2ffcd555323c30e6" - integrity sha512-ixm8NISFlPlEo3FjSaqmq4nnd13BRHoafwJ5MG+okCz6BKGZ1SexEggP42/QpGvDprUUHnfncG6WUMgcarr1zA== +"@redis/bloom@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== + +"@redis/client@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.6.0.tgz#dcf4ae1319763db6fdddd6de7f0af68a352c30ea" + integrity sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg== + dependencies: + cluster-key-slot "1.1.2" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" + integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw== + +"@redis/json@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.7.tgz#016257fcd933c4cbcb9c49cde8a0961375c6893b" + integrity sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ== + +"@redis/search@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.2.0.tgz#50976fd3f31168f585666f7922dde111c74567b8" + integrity sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw== + +"@redis/time-series@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.1.0.tgz#cba454c05ec201bd5547aaf55286d44682ac8eb5" + integrity sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g== + +"@sentry/core@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.25.0.tgz#f64e50b88ee5b13f1d52b543638b2eb5c8e326d8" + integrity sha512-7KtglbrW1eX4DOHkf6i4rRIExEf2CgtQ99qZ8gn5FUaAmNMg0rK7bb1yZMx0RZtp5G1TSz/S0jQQgxHWebaEig== dependencies: - "@sentry/types" "8.9.2" - "@sentry/utils" "8.9.2" + "@sentry/types" "8.25.0" + "@sentry/utils" "8.25.0" -"@sentry/node@^8.9.2": - version "8.9.2" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.9.2.tgz#67a95050c499542c963da7bf9815f16aa3163607" - integrity sha512-Q+JBpR4yx3eUyyhwgugucfRtPg65gYvzJGEmjzcnDJXJqX8ms4HPpNv9o2Om7A4014JxIibUdrQ+p5idcT7SZA== +"@sentry/node@^8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.25.0.tgz#1642e065082aba7e9dca5506a18366b2dbb4ce58" + integrity sha512-KFeJpYU/7CKi/v8D72ztniA+QqH0yBv2wzEP0PUe3DWZ/Fwl0OQSVWNNuDfJBQUvk3NrytCH5A6klZjU0/rwlw== dependencies: "@opentelemetry/api" "^1.9.0" - "@opentelemetry/context-async-hooks" "^1.25.0" - "@opentelemetry/core" "^1.25.0" - "@opentelemetry/instrumentation" "^0.52.0" - "@opentelemetry/instrumentation-connect" "0.37.0" - "@opentelemetry/instrumentation-express" "0.40.1" - "@opentelemetry/instrumentation-fastify" "0.37.0" - "@opentelemetry/instrumentation-graphql" "0.41.0" - "@opentelemetry/instrumentation-hapi" "0.39.0" - "@opentelemetry/instrumentation-http" "0.52.0" - "@opentelemetry/instrumentation-ioredis" "0.41.0" - "@opentelemetry/instrumentation-koa" "0.41.0" - "@opentelemetry/instrumentation-mongodb" "0.45.0" - "@opentelemetry/instrumentation-mongoose" "0.39.0" - "@opentelemetry/instrumentation-mysql" "0.39.0" - "@opentelemetry/instrumentation-mysql2" "0.39.0" - "@opentelemetry/instrumentation-nestjs-core" "0.38.0" - "@opentelemetry/instrumentation-pg" "0.42.0" - "@opentelemetry/instrumentation-redis-4" "0.40.0" - "@opentelemetry/resources" "^1.25.0" - "@opentelemetry/sdk-trace-base" "^1.25.0" - "@opentelemetry/semantic-conventions" "^1.25.0" - "@prisma/instrumentation" "5.15.0" - "@sentry/core" "8.9.2" - "@sentry/opentelemetry" "8.9.2" - "@sentry/types" "8.9.2" - "@sentry/utils" "8.9.2" + "@opentelemetry/context-async-hooks" "^1.25.1" + "@opentelemetry/core" "^1.25.1" + "@opentelemetry/instrumentation" "^0.52.1" + "@opentelemetry/instrumentation-connect" "0.38.0" + "@opentelemetry/instrumentation-express" "0.41.1" + "@opentelemetry/instrumentation-fastify" "0.38.0" + "@opentelemetry/instrumentation-graphql" "0.42.0" + "@opentelemetry/instrumentation-hapi" "0.40.0" + "@opentelemetry/instrumentation-http" "0.52.1" + "@opentelemetry/instrumentation-ioredis" "0.42.0" + "@opentelemetry/instrumentation-koa" "0.42.0" + "@opentelemetry/instrumentation-mongodb" "0.46.0" + "@opentelemetry/instrumentation-mongoose" "0.40.0" + "@opentelemetry/instrumentation-mysql" "0.40.0" + "@opentelemetry/instrumentation-mysql2" "0.40.0" + "@opentelemetry/instrumentation-nestjs-core" "0.39.0" + "@opentelemetry/instrumentation-pg" "0.43.0" + "@opentelemetry/instrumentation-redis-4" "0.41.0" + "@opentelemetry/resources" "^1.25.1" + "@opentelemetry/sdk-trace-base" "^1.25.1" + "@opentelemetry/semantic-conventions" "^1.25.1" + "@prisma/instrumentation" "5.17.0" + "@sentry/core" "8.25.0" + "@sentry/opentelemetry" "8.25.0" + "@sentry/types" "8.25.0" + "@sentry/utils" "8.25.0" + import-in-the-middle "^1.11.0" optionalDependencies: - opentelemetry-instrumentation-fetch-node "1.2.0" + opentelemetry-instrumentation-fetch-node "1.2.3" -"@sentry/opentelemetry@8.9.2": - version "8.9.2" - resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.9.2.tgz#64048025283db5099bcf9b8e4e60a9b68b729610" - integrity sha512-Q6SHDQhrsBPcMi7ejqVdNTkt6SCTIhpGsFN8QR7daH3uvM0X2O7ciCuO9gRNRTEkflEINV4SBZEjANYH7BkRAg== +"@sentry/opentelemetry@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.25.0.tgz#888425f08668c288611d4783346d16e364d964d2" + integrity sha512-6g4TXwQMHtvmlu2i1OKqvFD2W2RTrGBxDtJ1tBQmqCfHKyiqQ37gy6AozuwrQ3po1KKbawaQGIFNEzb4wnSrfA== dependencies: - "@sentry/core" "8.9.2" - "@sentry/types" "8.9.2" - "@sentry/utils" "8.9.2" + "@sentry/core" "8.25.0" + "@sentry/types" "8.25.0" + "@sentry/utils" "8.25.0" -"@sentry/types@8.9.2": - version "8.9.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.9.2.tgz#d143383fc35552d9f153042cc6d56c5ee8ec2fa6" - integrity sha512-+LFOyQGl+zk5SZRGZD2MEURf7i5RHgP/mt3s85Rza+vz8M211WJ0YsjkIGUJFSY842nged5QLx4JysLaBlLymg== +"@sentry/types@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.25.0.tgz#cde3d900efe7fb7614a670f0af2634a2cbd92693" + integrity sha512-ojim0gDcRhGJPguYrtms4FsprX4xZz3LGNk9Z0hwTbSVEdlhQIInsQ7CYcdM3sjUs+qT7kfpxTRZGUeZNRRJcA== -"@sentry/utils@8.9.2": - version "8.9.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.9.2.tgz#58b003d9c1302f61192e7c99ea42bf1cd5cad7f7" - integrity sha512-A4srR9mEBFdVXwSEKjQ94msUbVkMr8JeFiEj9ouOFORw/Y/ux/WV2bWVD/ZI9wq0TcTNK8L1wBgU8UMS5lIq3A== +"@sentry/utils@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.25.0.tgz#708ccf8b953f64e1a5915e09d4cb33105b29e436" + integrity sha512-mVlkV7S62ZZ2jM38/kOwWx2xoW8fUv2cjw2IwFKoAIPyLBh3mo1WJtvfdtN/rXGjQWZJBKW53EWaWnD00rkjyA== dependencies: - "@sentry/types" "8.9.2" + "@sentry/types" "8.25.0" "@sinclair/typebox@^0.24.1": version "0.24.19" @@ -2635,12 +2837,12 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@smithy/abort-controller@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-3.0.1.tgz#bb8debe1c23ca62a61b33a9ee2918f5a79d81928" - integrity sha512-Jb7jg4E+C+uvrUQi+h9kbILY6ts6fglKZzseMCHlH9ayq+1f5QdpYf8MV/xppuiN6DAMJAmwGz53GwP3213dmA== +"@smithy/abort-controller@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-3.1.1.tgz#291210611ff6afecfc198d0ca72d5771d8461d16" + integrity sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@smithy/chunked-blob-reader-native@^3.0.0": @@ -2658,133 +2860,133 @@ dependencies: tslib "^2.6.2" -"@smithy/config-resolver@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-3.0.2.tgz#ad19331d48d9a6e67bdd43a0099e1d8af1b82a82" - integrity sha512-wUyG6ezpp2sWAvfqmSYTROwFUmJqKV78GLf55WODrosBcT0BAMd9bOLO4HRhynWBgAobPml2cF9ZOdgCe00r+g== +"@smithy/config-resolver@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-3.0.5.tgz#727978bba7ace754c741c259486a19d3083431fd" + integrity sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA== dependencies: - "@smithy/node-config-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/types" "^3.3.0" "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.1" + "@smithy/util-middleware" "^3.0.3" tslib "^2.6.2" -"@smithy/core@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-2.2.1.tgz#92ed71eb96ef16d5ac8b23dbdf913bcb225ab875" - integrity sha512-R8Pzrr2v2oGUoj4CTZtKPr87lVtBsz7IUBGhSwS1kc6Cj0yPwNdYbkzhFsxhoDE9+BPl09VN/6rFsW9GJzWnBA== - dependencies: - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-retry" "^3.0.4" - "@smithy/middleware-serde" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/util-middleware" "^3.0.1" +"@smithy/core@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-2.3.2.tgz#4a1e3da41d2a3a494cbc6bd1fc6eeb26b2e27184" + integrity sha512-in5wwt6chDBcUv1Lw1+QzZxN9fBffi+qOixfb65yK4sDuKG7zAUO9HAFqmVzsZM3N+3tTyvZjtnDXePpvp007Q== + dependencies: + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-retry" "^3.0.14" + "@smithy/middleware-serde" "^3.0.3" + "@smithy/protocol-http" "^4.1.0" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/util-middleware" "^3.0.3" tslib "^2.6.2" -"@smithy/credential-provider-imds@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.1.tgz#8b2b3c9e7e67fd9e3e436a5e1db6652ab339af7b" - integrity sha512-htndP0LwHdE3R3Nam9ZyVWhwPYOmD4xCL79kqvNxy8u/bv0huuy574CSiRY4cvEICgimv8jlVfLeZ7zZqbnB2g== +"@smithy/credential-provider-imds@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz#0e0e7ddaff1a8633cb927aee1056c0ab506b7ecf" + integrity sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA== dependencies: - "@smithy/node-config-provider" "^3.1.1" - "@smithy/property-provider" "^3.1.1" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/property-provider" "^3.1.3" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" tslib "^2.6.2" -"@smithy/eventstream-codec@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.1.0.tgz#74138287be7e1edd6a72400bb5181f5e1a7b44dd" - integrity sha512-XFDl70ZY+FabSnTX3oQGGYvdbEaC8vPEFkCEOoBkumqaZIwR1WjjJCDu2VMXlHbKWKshefWXdT0NYteL5v6uFw== +"@smithy/eventstream-codec@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.1.2.tgz#4a1c72b34400631b829241151984a1ad8c4f963c" + integrity sha512-0mBcu49JWt4MXhrhRAlxASNy0IjDRFU+aWNDRal9OtUJvJNiwDuyKMUONSOjLjSCeGwZaE0wOErdqULer8r7yw== dependencies: "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-hex-encoding" "^3.0.0" tslib "^2.6.2" -"@smithy/eventstream-serde-browser@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.2.tgz#342fbdbdf99f8fb7c247024716c5236bffae043e" - integrity sha512-6147vdedQGaWn3Nt4P1KV0LuV8IH4len1SAeycyko0p8oRLWFyYyx0L8JHGclePDSphkjxZqBHtyIfyupCaTGg== +"@smithy/eventstream-serde-browser@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.6.tgz#a4ab4f7cfbd137bcaa54c375276f9214e568fd8f" + integrity sha512-2hM54UWQUOrki4BtsUI1WzmD13/SeaqT/AB3EUJKbcver/WgKNaiJ5y5F5XXuVe6UekffVzuUDrBZVAA3AWRpQ== dependencies: - "@smithy/eventstream-serde-universal" "^3.0.2" - "@smithy/types" "^3.1.0" + "@smithy/eventstream-serde-universal" "^3.0.5" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/eventstream-serde-config-resolver@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.1.tgz#74e9cb3992edc03319ffa05eb6008aacaaca4f71" - integrity sha512-6+B8P+5Q1mll4u7IoI7mpmYOSW3/c2r3WQoYLdqOjbIKMixJFGmN79ZjJiNMy4X2GZ4We9kQ6LfnFuczSlhcyw== +"@smithy/eventstream-serde-config-resolver@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.3.tgz#f852e096d0ad112363b4685e1d441088d1fce67a" + integrity sha512-NVTYjOuYpGfrN/VbRQgn31x73KDLfCXCsFdad8DiIc3IcdxL+dYA9zEQPyOP7Fy2QL8CPy2WE4WCUD+ZsLNfaQ== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/eventstream-serde-node@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.2.tgz#fff9e92983c97f07174c1bbcf7f1af47fc478a6e" - integrity sha512-DLtmGAfqxZAql8rB+HqyPlUne22u3EEVj+hxlUjgXk0hXt+SfLGK0ljzRFmiWQ3qGpHu1NdJpJA9e5JE/dJxFw== +"@smithy/eventstream-serde-node@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.5.tgz#2bbf5c9312a28f23bc55ae284efa9499f8b8f982" + integrity sha512-+upXvnHNyZP095s11jF5dhGw/Ihzqwl5G+/KtMnoQOpdfC3B5HYCcDVG9EmgkhJMXJlM64PyN5gjJl0uXFQehQ== dependencies: - "@smithy/eventstream-serde-universal" "^3.0.2" - "@smithy/types" "^3.1.0" + "@smithy/eventstream-serde-universal" "^3.0.5" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/eventstream-serde-universal@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.2.tgz#d1704c14b0a691d0d8b4f68def68adaa20bb96d8" - integrity sha512-d3SgAIQ/s4EbU8HAHJ8m2MMJPAL30nqJktyVgvqZWNznA8PJl61gJw5gj/yjIt/Fvs3d4fU8FmPPAhdp2yr/7A== +"@smithy/eventstream-serde-universal@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.5.tgz#e1cc2f71f4d174a03e00ce4b563395a81dd17bec" + integrity sha512-5u/nXbyoh1s4QxrvNre9V6vfyoLWuiVvvd5TlZjGThIikc3G+uNiG9uOTCWweSRjv1asdDIWK7nOmN7le4RYHQ== dependencies: - "@smithy/eventstream-codec" "^3.1.0" - "@smithy/types" "^3.1.0" + "@smithy/eventstream-codec" "^3.1.2" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/fetch-http-handler@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-3.0.2.tgz#eff4056e819b3591d1c5d472ee58c2981886920a" - integrity sha512-0nW6tLK0b7EqSsfKvnOmZCgJqnodBAnvqcrlC5dotKfklLedPTRGsQamSVbVDWyuU/QGg+YbZDJUQ0CUufJXZQ== +"@smithy/fetch-http-handler@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz#c754de7e0ff2541b73ac9ba7cc955940114b3d62" + integrity sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg== dependencies: - "@smithy/protocol-http" "^4.0.1" - "@smithy/querystring-builder" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/querystring-builder" "^3.0.3" + "@smithy/types" "^3.3.0" "@smithy/util-base64" "^3.0.0" tslib "^2.6.2" -"@smithy/hash-blob-browser@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.0.tgz#0002113c3214e1d4fef2c489ac7b15d0b141d2af" - integrity sha512-lKEHDN6bLzYdx5cFmdMHfYVmmTZTmjphwPBSumgkaniEYwRAXnbDEGETeuzfquS9Py1aH6cmqzXWxxkD7mV3sA== +"@smithy/hash-blob-browser@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.2.tgz#90281c1f183d93686fb4f26107f1819644d68829" + integrity sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg== dependencies: "@smithy/chunked-blob-reader" "^3.0.0" "@smithy/chunked-blob-reader-native" "^3.0.0" - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/hash-node@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-3.0.1.tgz#52924bcbd6a02c7f7e2d9c332f59d5adc09688a3" - integrity sha512-w2ncjgk2EYO2+WhAsSQA8owzoOSY7IL1qVytlwpnL1pFGWTjIoIh5nROkEKXY51unB63bMGZqDiVoXaFbyKDlg== +"@smithy/hash-node@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-3.0.3.tgz#82c5cb7b0f1a29ee7319081853d2d158c07dff24" + integrity sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-buffer-from" "^3.0.0" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/hash-stream-node@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-3.1.0.tgz#80fbd12b223869862e6ab3aecc5a8fb7064b884e" - integrity sha512-OkU9vjN17yYsXTSrouctZn2iYwG4z8WSc7F50+9ogG2crOtMopkop+22j35tX2ry2i/vLRCYgnqEmBWfvnYT2g== +"@smithy/hash-stream-node@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-3.1.2.tgz#89f0290ae44b113863878e75b10c484ff48af71c" + integrity sha512-PBgDMeEdDzi6JxKwbfBtwQG9eT9cVwsf0dZzLXoJF4sHKHs5HEo/3lJWpn6jibfJwT34I1EBXpBnZE8AxAft6g== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/invalid-dependency@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-3.0.1.tgz#921787acfbe136af7ded46ae6f4b3d81c9b7e05e" - integrity sha512-RSNF/32BKygXKKMyS7koyuAq1rcdW5p5c4EFa77QenBFze9As+JiRnV9OWBh2cB/ejGZalEZjvIrMLHwJl7aGA== +"@smithy/invalid-dependency@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz#8d9fd70e3a94b565a4eba4ffbdc95238e1930528" + integrity sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@smithy/is-array-buffer@^2.2.0": @@ -2801,176 +3003,177 @@ dependencies: tslib "^2.6.2" -"@smithy/md5-js@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-3.0.1.tgz#796dca16509f66da5ba380120efdbbbfc4d1ab5d" - integrity sha512-wQa0YGsR4Zb1GQLGwOOgRAbkj22P6CFGaFzu5bKk8K4HVNIC2dBlIxqZ/baF0pLiSZySAPdDZT7CdZ7GkGXt5A== +"@smithy/md5-js@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-3.0.3.tgz#55ee40aa24075b096c39f7910590c18ff7660c98" + integrity sha512-O/SAkGVwpWmelpj/8yDtsaVe6sINHLB1q8YE/+ZQbDxIw3SRLbTZuRaI10K12sVoENdnHqzPp5i3/H+BcZ3m3Q== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/middleware-content-length@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-3.0.1.tgz#90bce78dfd0db978df7920ae58e420ce9ed2f79a" - integrity sha512-6QdK/VbrCfXD5/QolE2W/ok6VqxD+SM28Ds8iSlEHXZwv4buLsvWyvoEEy0322K/g5uFgPzBmZjGqesTmPL+yQ== +"@smithy/middleware-content-length@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz#1680aa4fb2a1c0505756103c9a5c2916307d9035" + integrity sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw== dependencies: - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/middleware-endpoint@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.2.tgz#93bb575a25bb0bd5d1d18cd77157ccb2ba15112a" - integrity sha512-gWEaGYB3Bei17Oiy/F2IlUPpBazNXImytoOdJ1xbrUOaJKAOiUhx8/4FOnYLLJHdAwa9PlvJ2ULda2f/Dnwi9w== - dependencies: - "@smithy/middleware-serde" "^3.0.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" - "@smithy/url-parser" "^3.0.1" - "@smithy/util-middleware" "^3.0.1" +"@smithy/middleware-endpoint@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz#9b8a496d87a68ec43f3f1a0139868d6765a88119" + integrity sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw== + dependencies: + "@smithy/middleware-serde" "^3.0.3" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" + "@smithy/url-parser" "^3.0.3" + "@smithy/util-middleware" "^3.0.3" tslib "^2.6.2" -"@smithy/middleware-retry@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-3.0.4.tgz#4f1a23c218fe279659c3d88ec1c18bf19938eba6" - integrity sha512-Tu+FggbLNF5G9L6Wi8o32Mg4bhlBInWlhhaFKyytGRnkfxGopxFVXJQn7sjZdFYJyTz6RZZa06tnlvavUgtoVg== - dependencies: - "@smithy/node-config-provider" "^3.1.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/service-error-classification" "^3.0.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" - "@smithy/util-middleware" "^3.0.1" - "@smithy/util-retry" "^3.0.1" +"@smithy/middleware-retry@^3.0.14": + version "3.0.14" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-3.0.14.tgz#739e8bac6e465e0cda26446999db614418e79da3" + integrity sha512-7ZaWZJOjUxa5hgmuMspyt8v/zVsh0GXYuF7OvCmdcbVa/xbnKQoYC+uYKunAqRGTkxjOyuOCw9rmFUFOqqC0eQ== + dependencies: + "@smithy/node-config-provider" "^3.1.4" + "@smithy/protocol-http" "^4.1.0" + "@smithy/service-error-classification" "^3.0.3" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" + "@smithy/util-middleware" "^3.0.3" + "@smithy/util-retry" "^3.0.3" tslib "^2.6.2" uuid "^9.0.1" -"@smithy/middleware-serde@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-3.0.1.tgz#566ec46ee84873108c1cea26b3f3bd2899a73249" - integrity sha512-ak6H/ZRN05r5+SR0/IUc5zOSyh2qp3HReg1KkrnaSLXmncy9lwOjNqybX4L4x55/e5mtVDn1uf/gQ6bw5neJPw== +"@smithy/middleware-serde@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz#74d974460f74d99f38c861e6862984543a880a66" + integrity sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/middleware-stack@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-3.0.1.tgz#9418f1295efda318c181bf3bca65173a75d133e5" - integrity sha512-fS5uT//y1SlBdkzIvgmWQ9FufwMXrHSSbuR25ygMy1CRDIZkcBMoF4oTMYNfR9kBlVBcVzlv7joFdNrFuQirPA== +"@smithy/middleware-stack@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz#91845c7e61e6f137fa912b623b6def719a4f6ce7" + integrity sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/node-config-provider@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-3.1.1.tgz#a361ab228d2229b03cc2fbdfd304055c38127614" - integrity sha512-z5G7+ysL4yUtMghUd2zrLkecu0mTfnYlt5dR76g/HsFqf7evFazwiZP1ag2EJenGxNBDwDM5g8nm11NPogiUVA== +"@smithy/node-config-provider@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-3.1.4.tgz#05647bed666aa8036a1ad72323c1942e5d421be1" + integrity sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ== dependencies: - "@smithy/property-provider" "^3.1.1" - "@smithy/shared-ini-file-loader" "^3.1.1" - "@smithy/types" "^3.1.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/shared-ini-file-loader" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/node-http-handler@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-3.0.1.tgz#40e1ebe00aeb628a46a3a12b14ad6cabb69b576e" - integrity sha512-hlBI6MuREA4o1wBMEt+QNhUzoDtFFvwR6ecufimlx9D79jPybE/r8kNorphXOi91PgSO9S2fxRjcKCLk7Jw8zA== +"@smithy/node-http-handler@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz#be4195e45639e690d522cd5f11513ea822ff9d5f" + integrity sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg== dependencies: - "@smithy/abort-controller" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/querystring-builder" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/abort-controller" "^3.1.1" + "@smithy/protocol-http" "^4.1.0" + "@smithy/querystring-builder" "^3.0.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/property-provider@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-3.1.1.tgz#4849b69b83ac97e68e80d2dc0c2b98ce5950dffe" - integrity sha512-YknOMZcQkB5on+MU0DvbToCmT2YPtTETMXW0D3+/Iln7ezT+Zm1GMHhCW1dOH/X/+LkkQD9aXEoCX/B10s4Xdw== +"@smithy/property-provider@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-3.1.3.tgz#afd57ea82a3f6c79fbda95e3cb85c0ee0a79f39a" + integrity sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/protocol-http@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.0.1.tgz#7b57080565816f229d2391726f537e13371c7e38" - integrity sha512-eBhm9zwcFPEazc654c0BEWtxYAzrw+OhoSf5pkwKzfftWKXRoqEhwOE2Pvn30v0iAdo7Mfsfb6pi1NnZlGCMpg== +"@smithy/protocol-http@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.1.0.tgz#23519d8f45bf4f33960ea5415847bc2b620a010b" + integrity sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/querystring-builder@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-3.0.1.tgz#8fb20e1d13154661612954c5ba448e0875be6118" - integrity sha512-vKitpnG/2KOMVlx3x1S3FkBH075EROG3wcrcDaNerQNh8yuqnSL23btCD2UyX4i4lpPzNW6VFdxbn2Z25b/g5Q== +"@smithy/querystring-builder@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz#6b0e566f885bb84938d077c69e8f8555f686af13" + integrity sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-uri-escape" "^3.0.0" tslib "^2.6.2" -"@smithy/querystring-parser@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-3.0.1.tgz#68589196fedf280aad2c0a69a2a016f78b2137cf" - integrity sha512-Qt8DMC05lVS8NcQx94lfVbZSX+2Ym7032b/JR8AlboAa/D669kPzqb35dkjkvAG6+NWmUchef3ENtrD6F+5n8Q== +"@smithy/querystring-parser@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz#272a6b83f88dfcbbec8283d72a6bde850cc00091" + integrity sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/service-error-classification@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-3.0.1.tgz#23db475d3cef726e8bf3435229e6e04e4de92430" - integrity sha512-ubFUvIePjDCyIzZ+pLETqNC6KXJ/fc6g+/baqel7Zf6kJI/kZKgjwkCI7zbUhoUuOZ/4eA/87YasVu40b/B4bA== +"@smithy/service-error-classification@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz#73484255060a094aa9372f6cd972dcaf97e3ce80" + integrity sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" -"@smithy/shared-ini-file-loader@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.1.tgz#752ecd8962a660ded75d25341a48feb94f145a6f" - integrity sha512-nD6tXIX2126/P9e3wqRY1bm9dTtPZwRDyjVOd18G28o+1UOG+kOVgUwujE795HslSuPlEgqzsH5sgNP1hDjj9g== +"@smithy/shared-ini-file-loader@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.4.tgz#7dceaf5a5307a2ee347ace8aba17312a1a3ede15" + integrity sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/signature-v4@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-3.1.0.tgz#cc819568c4fcbadce107901680a96e662bccc86a" - integrity sha512-m0/6LW3IQ3/JBcdhqjpkpABPTPhcejqeAn0U877zxBdNLiWAnG2WmCe5MfkUyVuvpFTPQnQwCo/0ZBR4uF5kxg== +"@smithy/signature-v4@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-4.1.0.tgz#251ff43dc1f4ad66776122732fea9e56efc56443" + integrity sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag== dependencies: "@smithy/is-array-buffer" "^3.0.0" - "@smithy/types" "^3.1.0" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" "@smithy/util-hex-encoding" "^3.0.0" - "@smithy/util-middleware" "^3.0.1" + "@smithy/util-middleware" "^3.0.3" "@smithy/util-uri-escape" "^3.0.0" "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/smithy-client@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-3.1.2.tgz#1c27ab4910bbfd6c0bc04ddd8412494e7a7daba7" - integrity sha512-f3eQpczBOFUtdT/ptw2WpUKu1qH1K7xrssrSiHYtd9TuLXkvFqb88l9mz9FHeUVNSUxSnkW1anJnw6rLwUKzQQ== - dependencies: - "@smithy/middleware-endpoint" "^3.0.2" - "@smithy/middleware-stack" "^3.0.1" - "@smithy/protocol-http" "^4.0.1" - "@smithy/types" "^3.1.0" - "@smithy/util-stream" "^3.0.2" +"@smithy/smithy-client@^3.1.12": + version "3.1.12" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-3.1.12.tgz#fb6386816ff8a5c50eab7503d4ee3ba2e4ebac63" + integrity sha512-wtm8JtsycthkHy1YA4zjIh2thJgIQ9vGkoR639DBx5lLlLNU0v4GARpQZkr2WjXue74nZ7MiTSWfVrLkyD8RkA== + dependencies: + "@smithy/middleware-endpoint" "^3.1.0" + "@smithy/middleware-stack" "^3.0.3" + "@smithy/protocol-http" "^4.1.0" + "@smithy/types" "^3.3.0" + "@smithy/util-stream" "^3.1.3" tslib "^2.6.2" -"@smithy/types@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.1.0.tgz#e2eb2e2130026a8a0631b2605c17df1975aa99d6" - integrity sha512-qi4SeCVOUPjhSSZrxxB/mB8DrmuSFUcJnD9KXjuP+7C3LV/KFV4kpuUSH3OHDZgQB9TEH/1sO/Fq/5HyaK9MPw== +"@smithy/types@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.3.0.tgz#fae037c733d09bc758946a01a3de0ef6e210b16b" + integrity sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA== dependencies: tslib "^2.6.2" -"@smithy/url-parser@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-3.0.1.tgz#5451fc7034e9eda112696d1a9508746a7f8b0521" - integrity sha512-G140IlNFlzYWVCedC4E2d6NycM1dCUbe5CnsGW1hmGt4hYKiGOw0v7lVru9WAn5T2w09QEjl4fOESWjGmCvVmg== +"@smithy/url-parser@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-3.0.3.tgz#e8a060d9810b24b1870385fc2b02485b8a6c5955" + integrity sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A== dependencies: - "@smithy/querystring-parser" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/querystring-parser" "^3.0.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@smithy/util-base64@^3.0.0": @@ -3019,37 +3222,37 @@ dependencies: tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.4.tgz#4392db3d96aa08ae161bb987ecfedc094d84b88d" - integrity sha512-sXtin3Mue3A3xo4+XkozpgPptgmRwvNPOqTvb3ANGTCzzoQgAPBNjpE+aXCINaeSMXwHmv7E2oEn2vWdID+SAQ== +"@smithy/util-defaults-mode-browser@^3.0.14": + version "3.0.14" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.14.tgz#21f3ebcb07b9d6ae1274b9d655c38bdac59e5c06" + integrity sha512-0iwTgKKmAIf+vFLV8fji21Jb2px11ktKVxbX6LIDPAUJyWQqGqBVfwba7xwa1f2FZUoolYQgLvxQEpJycXuQ5w== dependencies: - "@smithy/property-provider" "^3.1.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" + "@smithy/property-provider" "^3.1.3" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" bowser "^2.11.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.4.tgz#794b8bb3facb5f6581af8d02fcf1b42b34c103e5" - integrity sha512-CUF6TyxLh3CgBRVYgZNOPDfzHQjeQr0vyALR6/DkQkOm7rNfGEzW1BRFi88C73pndmfvoiIT7ochuT76OPz9Dw== - dependencies: - "@smithy/config-resolver" "^3.0.2" - "@smithy/credential-provider-imds" "^3.1.1" - "@smithy/node-config-provider" "^3.1.1" - "@smithy/property-provider" "^3.1.1" - "@smithy/smithy-client" "^3.1.2" - "@smithy/types" "^3.1.0" +"@smithy/util-defaults-mode-node@^3.0.14": + version "3.0.14" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.14.tgz#6bb9e837282e84bbf5093dbcd120fcd296593f7a" + integrity sha512-e9uQarJKfXApkTMMruIdxHprhcXivH1flYCe8JRDTzkkLx8dA3V5J8GZlST9yfDiRWkJpZJlUXGN9Rc9Ade3OQ== + dependencies: + "@smithy/config-resolver" "^3.0.5" + "@smithy/credential-provider-imds" "^3.2.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/property-provider" "^3.1.3" + "@smithy/smithy-client" "^3.1.12" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/util-endpoints@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-2.0.2.tgz#f995cca553569af43bef82f59d63b4969516df95" - integrity sha512-4zFOcBFQvifd2LSD4a1dKvfIWWwh4sWNtS3oZ7mpob/qPPmJseqKB148iT+hWCDsG//TmI+8vjYPgZdvnkYlTg== +"@smithy/util-endpoints@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-2.0.5.tgz#e3a7a4d1c41250bfd2b2d890d591273a7d8934be" + integrity sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg== dependencies: - "@smithy/node-config-provider" "^3.1.1" - "@smithy/types" "^3.1.0" + "@smithy/node-config-provider" "^3.1.4" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@smithy/util-hex-encoding@^3.0.0": @@ -3059,31 +3262,31 @@ dependencies: tslib "^2.6.2" -"@smithy/util-middleware@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.1.tgz#3e0eabaf936e62651a0b9a7c7c3bbe43d3971c91" - integrity sha512-WRODCQtUsO7vIvfrdxS8RFPeLKcewYtaCglZsBsedIKSUGIIvMlZT5oh+pCe72I+1L+OjnZuqRNpN2LKhWA4KQ== +"@smithy/util-middleware@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.3.tgz#07bf9602682f5a6c55bc2f0384303f85fc68c87e" + integrity sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw== dependencies: - "@smithy/types" "^3.1.0" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/util-retry@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-3.0.1.tgz#24037ff87a314a1ac99f80da43f579ae2352fe18" - integrity sha512-5lRtYm+8fNFEUTdqZXg5M4ppVp40rMIJfR1TpbHAhKQgPIDpWT+iYMaqgnwEbtpi9U1smyUOPv5Sg+M1neOBgw== +"@smithy/util-retry@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-3.0.3.tgz#9b2ac0dbb1c81f69812a8affa4d772bebfc0e049" + integrity sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w== dependencies: - "@smithy/service-error-classification" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/service-error-classification" "^3.0.3" + "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@smithy/util-stream@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-3.0.2.tgz#ed1377bfe824d8acfc105ab2d17ec4f376382cb2" - integrity sha512-n5Obp5AnlI6qHo8sbupwrcpBe6vFp4qkl0SRNuExKPNrH3ABAMG2ZszRTIUIv2b4AsFrCO+qiy4uH1Q3z1dxTA== +"@smithy/util-stream@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-3.1.3.tgz#699ee2397cc1d474e46d2034039d5263812dca64" + integrity sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw== dependencies: - "@smithy/fetch-http-handler" "^3.0.2" - "@smithy/node-http-handler" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/fetch-http-handler" "^3.2.4" + "@smithy/node-http-handler" "^3.1.4" + "@smithy/types" "^3.3.0" "@smithy/util-base64" "^3.0.0" "@smithy/util-buffer-from" "^3.0.0" "@smithy/util-hex-encoding" "^3.0.0" @@ -3113,13 +3316,13 @@ "@smithy/util-buffer-from" "^3.0.0" tslib "^2.6.2" -"@smithy/util-waiter@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-3.0.1.tgz#62d8ff58374032aa8c7e573b1ca4234407c605bd" - integrity sha512-wwnrVQdjQxvWGOAiLmqlEhENGCcDIN+XJ/+usPOgSZObAslrCXgKlkX7rNVwIWW2RhPguTKthvF+4AoO0Z6KpA== +"@smithy/util-waiter@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-3.1.2.tgz#2d40c3312f3537feee763459a19acafab4c75cf3" + integrity sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw== dependencies: - "@smithy/abort-controller" "^3.0.1" - "@smithy/types" "^3.1.0" + "@smithy/abort-controller" "^3.1.1" + "@smithy/types" "^3.3.0" tslib "^2.6.2" "@ts-morph/common@~0.12.3": @@ -3152,13 +3355,6 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== -"@types/accepts@*": - version "1.3.7" - resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265" - integrity sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ== - dependencies: - "@types/node" "*" - "@types/babel__core@^7.1.14": version "7.1.19" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" @@ -3224,11 +3420,6 @@ dependencies: "@types/node" "*" -"@types/content-disposition@*": - version "0.5.8" - resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537" - integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg== - "@types/conventional-commits-parser@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8" @@ -3241,16 +3432,6 @@ resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== -"@types/cookies@*": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.0.tgz#a2290cfb325f75f0f28720939bee854d4142aee2" - integrity sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q== - dependencies: - "@types/connect" "*" - "@types/express" "*" - "@types/keygrip" "*" - "@types/node" "*" - "@types/cors@^2.8.17": version "2.8.17" resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" @@ -3258,14 +3439,6 @@ dependencies: "@types/node" "*" -"@types/cron@^2.0.1": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/cron/-/cron-2.0.1.tgz" - integrity sha512-WHa/1rtNtD2Q/H0+YTTZoty+/5rcE66iAFX2IY+JuUoOACsevYyFkSYu/2vdw+G5LrmO7Lxowrqm0av4k3qWNQ== - dependencies: - "@types/luxon" "*" - "@types/node" "*" - "@types/crypto-js@^4.2.2": version "4.2.2" resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz" @@ -3349,16 +3522,6 @@ dependencies: "@types/node" "*" -"@types/http-assert@*": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf" - integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g== - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" @@ -3412,67 +3575,10 @@ dependencies: "@types/node" "*" -"@types/keygrip@*": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.6.tgz#1749535181a2a9b02ac04a797550a8787345b740" - integrity sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ== - -"@types/koa-compose@*": - version "3.2.8" - resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.8.tgz#dec48de1f6b3d87f87320097686a915f1e954b57" - integrity sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA== - dependencies: - "@types/koa" "*" - -"@types/koa@*": - version "2.15.0" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.15.0.tgz#eca43d76f527c803b491731f95df575636e7b6f2" - integrity sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g== - dependencies: - "@types/accepts" "*" - "@types/content-disposition" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/http-errors" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - -"@types/koa@2.14.0": - version "2.14.0" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.14.0.tgz#8939e8c3b695defc12f2ef9f38064509e564be18" - integrity sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA== - dependencies: - "@types/accepts" "*" - "@types/content-disposition" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/http-errors" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - -"@types/koa__router@12.0.3": - version "12.0.3" - resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-12.0.3.tgz#3fb74ea1991cadd6c6712b6106657aa6e64afca4" - integrity sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw== - dependencies: - "@types/koa" "*" - -"@types/lodash@^4.17.5": - version "4.17.5" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04" - integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw== - -"@types/luxon@*": - version "2.3.2" - resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz" - integrity sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA== - -"@types/luxon@~3.4.0": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" - integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA== +"@types/lodash@^4.17.7": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== "@types/methods@^1.1.4": version "1.1.4" @@ -3508,12 +3614,12 @@ resolved "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz" integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== -"@types/node@^20.14.2": - version "20.14.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" - integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== +"@types/node@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.2.0.tgz#7cf046a99f0ba4d628ad3088cb21f790df9b0c5b" + integrity sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ== dependencies: - undici-types "~5.26.4" + undici-types "~6.13.0" "@types/parse-json@^4.0.0": version "4.0.0" @@ -3621,10 +3727,10 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" -"@types/uuid@^9.0.8": - version "9.0.8" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" - integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== "@types/validator@^13.11.8": version "13.11.8" @@ -3655,62 +3761,62 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz#3cdeb5d44d051b21a9567535dd90702b2a42c6ff" - integrity sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w== +"@typescript-eslint/eslint-plugin@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz#3c020deeaaba82a6f741d00dacf172c53be4911f" + integrity sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.13.0" - "@typescript-eslint/type-utils" "7.13.0" - "@typescript-eslint/utils" "7.13.0" - "@typescript-eslint/visitor-keys" "7.13.0" + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/type-utils" "8.1.0" + "@typescript-eslint/utils" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.13.0.tgz#9489098d68d57ad392f507495f2b82ce8b8f0a6b" - integrity sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA== +"@typescript-eslint/parser@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.1.0.tgz#b7e77f5fa212df59eba51ecd4986f194bccc2303" + integrity sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA== dependencies: - "@typescript-eslint/scope-manager" "7.13.0" - "@typescript-eslint/types" "7.13.0" - "@typescript-eslint/typescript-estree" "7.13.0" - "@typescript-eslint/visitor-keys" "7.13.0" + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/typescript-estree" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz#6927d6451537ce648c6af67a2327378d4cc18462" - integrity sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng== +"@typescript-eslint/scope-manager@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz#dd8987d2efebb71d230a1c71d82e84a7aead5c3d" + integrity sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ== dependencies: - "@typescript-eslint/types" "7.13.0" - "@typescript-eslint/visitor-keys" "7.13.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" -"@typescript-eslint/type-utils@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz#4587282b5227a23753ea8b233805ecafc3924c76" - integrity sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A== +"@typescript-eslint/type-utils@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz#dbf5a4308166dfc37a36305390dea04a3a3b5048" + integrity sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA== dependencies: - "@typescript-eslint/typescript-estree" "7.13.0" - "@typescript-eslint/utils" "7.13.0" + "@typescript-eslint/typescript-estree" "8.1.0" + "@typescript-eslint/utils" "8.1.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.13.0.tgz#0cca95edf1f1fdb0cfe1bb875e121b49617477c5" - integrity sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA== +"@typescript-eslint/types@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.1.0.tgz#fbf1eaa668a7e444ac507732ca9d3c3468e5db9c" + integrity sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog== -"@typescript-eslint/typescript-estree@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz#4cc24fc155088ebf3b3adbad62c7e60f72c6de1c" - integrity sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw== +"@typescript-eslint/typescript-estree@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz#c44e5667683c0bb5caa43192e27de6a994f4e4c4" + integrity sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg== dependencies: - "@typescript-eslint/types" "7.13.0" - "@typescript-eslint/visitor-keys" "7.13.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -3718,22 +3824,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.13.0.tgz#f84e7e8aeceae945a9a3f40d077fd95915308004" - integrity sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ== +"@typescript-eslint/utils@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.1.0.tgz#a922985a43d2560ce0d293be79148fa80c1325e0" + integrity sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.13.0" - "@typescript-eslint/types" "7.13.0" - "@typescript-eslint/typescript-estree" "7.13.0" + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/typescript-estree" "8.1.0" -"@typescript-eslint/visitor-keys@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz#2eb7ce8eb38c2b0d4a494d1fe1908e7071a1a353" - integrity sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw== +"@typescript-eslint/visitor-keys@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz#ab2b3a9699a8ddebf0c205e133f114c1fed9daad" + integrity sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag== dependencies: - "@typescript-eslint/types" "7.13.0" + "@typescript-eslint/types" "8.1.0" eslint-visitor-keys "^3.4.3" "@ucast/core@^1.0.0", "@ucast/core@^1.4.1", "@ucast/core@^1.6.1": @@ -3764,125 +3870,125 @@ dependencies: "@ucast/core" "^1.4.1" -"@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz" - integrity sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: - "@webassemblyjs/helper-numbers" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz" - integrity sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz" - integrity sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz" - integrity sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== -"@webassemblyjs/helper-numbers@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz" - integrity sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.5" - "@webassemblyjs/helper-api-error" "1.11.5" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz" - integrity sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz" - integrity sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" -"@webassemblyjs/ieee754@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz" - integrity sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz" - integrity sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz" - integrity sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ== - -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz" - integrity sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/helper-wasm-section" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" - "@webassemblyjs/wasm-opt" "1.11.5" - "@webassemblyjs/wasm-parser" "1.11.5" - "@webassemblyjs/wast-printer" "1.11.5" - -"@webassemblyjs/wasm-gen@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz" - integrity sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/ieee754" "1.11.5" - "@webassemblyjs/leb128" "1.11.5" - "@webassemblyjs/utf8" "1.11.5" - -"@webassemblyjs/wasm-opt@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz" - integrity sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" - "@webassemblyjs/wasm-parser" "1.11.5" - -"@webassemblyjs/wasm-parser@1.11.5", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz" - integrity sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-api-error" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/ieee754" "1.11.5" - "@webassemblyjs/leb128" "1.11.5" - "@webassemblyjs/utf8" "1.11.5" - -"@webassemblyjs/wast-printer@1.11.5": - version "1.11.5" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz" - integrity sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA== - dependencies: - "@webassemblyjs/ast" "1.11.5" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -3941,6 +4047,11 @@ acorn@^8.11.3, acorn@^8.8.2: resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" + integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== + acorn@^8.4.1: version "8.7.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" @@ -4024,10 +4135,12 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" -ansi-escapes@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" - integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" ansi-regex@^5.0.1: version "5.0.1" @@ -4118,15 +4231,20 @@ asap@^2.0.0: resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +async@^3.2.3: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" - integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== +axios@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85" + integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -4212,6 +4330,15 @@ bcryptjs@^2.4.3: resolved "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz" integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== +bent@~7.3.6: + version "7.3.12" + resolved "https://registry.yarnpkg.com/bent/-/bent-7.3.12.tgz#e0a2775d4425e7674c64b78b242af4f49da6b035" + integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== + dependencies: + bytesish "^0.4.1" + caseless "~0.12.0" + is-stream "^2.0.0" + bignumber.js@^9.0.0: version "9.1.2" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz" @@ -4354,6 +4481,19 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bullmq@^5.12.5: + version "5.12.5" + resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-5.12.5.tgz#09486440c50b75f6c9c5cbcbda0ec7540fd03436" + integrity sha512-lchCvFuPdaIbq01qnyS7MOt2piPeCDHzCqIxNAQEgDSzZ+Eb4RBboUUMgmW90UtMjV46mEqsWY9B1l/7/C13SA== + dependencies: + cron-parser "^4.6.0" + ioredis "^5.4.1" + msgpackr "^1.10.1" + node-abort-controller "^3.1.1" + semver "^7.5.4" + tslib "^2.0.0" + uuid "^9.0.0" + busboy@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" @@ -4366,6 +4506,28 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +bytesish@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" + integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== + +cache-manager-redis-store@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz#8eeb211212763d04cef4058666182d624f714299" + integrity sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ== + dependencies: + redis "^4.3.1" + +cache-manager@^5.7.6: + version "5.7.6" + resolved "https://registry.yarnpkg.com/cache-manager/-/cache-manager-5.7.6.tgz#bdd8a154c73e5233824aa09ceb359ed225d37b7e" + integrity sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w== + dependencies: + eventemitter3 "^5.0.1" + lodash.clonedeep "^4.5.0" + lru-cache "^10.2.2" + promise-coalesce "^1.1.2" + call-bind@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -4374,14 +4536,16 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-bind@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz" - integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.1" - set-function-length "^1.1.1" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" @@ -4413,6 +4577,11 @@ case@^1.6.3: resolved "https://registry.npmjs.org/case/-/case-1.6.3.tgz" integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + cfb@~1.2.1: version "1.2.2" resolved "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz" @@ -4428,7 +4597,7 @@ chalk-template@^1.1.0: dependencies: chalk "^5.2.0" -chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2, chalk@~4.1.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4436,7 +4605,7 @@ chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4549,22 +4718,22 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== dependencies: - restore-cursor "^4.0.0" + restore-cursor "^5.0.0" cli-spinners@^2.5.0: version "2.6.1" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== -cli-table3@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== +cli-table3@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== dependencies: string-width "^4.2.0" optionalDependencies: @@ -4611,6 +4780,11 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" @@ -4687,7 +4861,7 @@ commander@^6.2.1: resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -comment-json@4.2.3, comment-json@^4.2.3: +comment-json@4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz" integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw== @@ -4698,6 +4872,17 @@ comment-json@4.2.3, comment-json@^4.2.3: has-own-prop "^2.0.0" repeat-string "^1.6.1" +comment-json@^4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.5.tgz#482e085f759c2704b60bc6f97f55b8c01bc41e70" + integrity sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw== + dependencies: + array-timsort "^1.0.3" + core-util-is "^1.0.3" + esprima "^4.0.1" + has-own-prop "^2.0.0" + repeat-string "^1.6.1" + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -4878,13 +5063,12 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cron@3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.7.tgz#3423d618ba625e78458fff8cb67001672d49ba0d" - integrity sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw== +cron-parser@^4.6.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== dependencies: - "@types/luxon" "~3.4.0" - luxon "~3.4.0" + luxon "^3.2.1" cross-inspect@1.0.0: version "1.0.0" @@ -4918,116 +5102,121 @@ crypto-js@^4.2.0: resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== -cspell-config-lib@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-config-lib/-/cspell-config-lib-8.8.4.tgz#72cb7052e5c9afe0627860719ac86852f409c4f7" - integrity sha512-Xf+aL669Cm+MYZTZULVWRQXB7sRWx9qs0hPrgqxeaWabLUISK57/qwcI24TPVdYakUCoud9Nv+woGi5FcqV5ZQ== +cspell-config-lib@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-config-lib/-/cspell-config-lib-8.13.3.tgz#06e574328d701dc34410f4b8fd14e8fd07b4d152" + integrity sha512-dzVdar8Kenwxho0PnUxOxwjUvyFYn6Q9mQAMHcQNXQrvo32bdpoF+oNtWC/5FfrQgUgyl19CVQ607bRigYWoOQ== dependencies: - "@cspell/cspell-types" "8.8.4" - comment-json "^4.2.3" - yaml "^2.4.3" + "@cspell/cspell-types" "8.13.3" + comment-json "^4.2.5" + yaml "^2.5.0" -cspell-dictionary@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-8.8.4.tgz#9db953707abcccc5177073ae298141944566baf7" - integrity sha512-eDi61MDDZycS5EASz5FiYKJykLEyBT0mCvkYEUCsGVoqw8T9gWuWybwwqde3CMq9TOwns5pxGcFs2v9RYgtN5A== +cspell-dictionary@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-8.13.3.tgz#622798ef6f8575d9ceb21dba20e8eace4177f74b" + integrity sha512-DQ3Tee7LIoy+9Mu52ht32O/MNBZ6i4iUeSTY2sMDDwogno3361BLRyfEjyiYNo3Fqf0Pcnt5MqY2DqIhrF/H/Q== dependencies: - "@cspell/cspell-pipe" "8.8.4" - "@cspell/cspell-types" "8.8.4" - cspell-trie-lib "8.8.4" + "@cspell/cspell-pipe" "8.13.3" + "@cspell/cspell-types" "8.13.3" + cspell-trie-lib "8.13.3" fast-equals "^5.0.1" - gensequence "^7.0.0" -cspell-gitignore@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-8.8.4.tgz#6762c9fb7d7cadb007659174efaeb448357cc924" - integrity sha512-rLdxpBh0kp0scwqNBZaWVnxEVmSK3UWyVSZmyEL4jmmjusHYM9IggfedOhO4EfGCIdQ32j21TevE0tTslyc4iA== +cspell-gitignore@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-8.13.3.tgz#d9bb175feb86899ed2e2af1bd714785f02b08aaa" + integrity sha512-0OZXuP33CXV4P95ySHGNqhq3VR5RaLwpyo0nGvLHOjPm3mCsQSjURLBKHvyQ3r2M7LWsGV1Xc81FfTx30FBZLg== dependencies: - cspell-glob "8.8.4" + "@cspell/url" "8.13.3" + cspell-glob "8.13.3" + cspell-io "8.13.3" find-up-simple "^1.0.0" -cspell-glob@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-8.8.4.tgz#b10af55ff306b9ad5114c8a2c54414f3f218d47a" - integrity sha512-+tRrOfTSbF/44uNl4idMZVPNfNM6WTmra4ZL44nx23iw1ikNhqZ+m0PC1oCVSlURNBEn8faFXjC/oT2BfgxoUQ== +cspell-glob@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-8.13.3.tgz#7533828ac52eb7ae87e8d58869ffc6fb9299a24e" + integrity sha512-+jGIMYyKDLmoOJIxNPXRdI7utcvw+9FMSmj1ApIdEff5dCkehi0gtzK4H7orXGYEvRdKQvfaXiyduVi79rXsZQ== dependencies: + "@cspell/url" "8.13.3" micromatch "^4.0.7" -cspell-grammar@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-8.8.4.tgz#91212b7210d9bf9c2fd21d604c589ca87a90a261" - integrity sha512-UxDO517iW6vs/8l4OhLpdMR7Bp+tkquvtld1gWz8WYQiDwORyf0v5a3nMh4ILYZGoolOSnDuI9UjWOLI6L/vvQ== - dependencies: - "@cspell/cspell-pipe" "8.8.4" - "@cspell/cspell-types" "8.8.4" - -cspell-io@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-8.8.4.tgz#a970ed76f06aebc9b64a1591024a4a854c7eb8c1" - integrity sha512-aqB/QMx+xns46QSyPEqi05uguCSxvqRnh2S/ZOhhjPlKma/7hK9niPRcwKwJXJEtNzdiZZkkC1uZt9aJe/7FTA== - dependencies: - "@cspell/cspell-service-bus" "8.8.4" - -cspell-lib@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-8.8.4.tgz#3af88990585a7e6a5f03bbf738b4434587e94cce" - integrity sha512-hK8gYtdQ9Lh86c8cEHITt5SaoJbfvXoY/wtpR4k393YR+eAxKziyv8ihQyFE/Z/FwuqtNvDrSntP9NLwTivd3g== - dependencies: - "@cspell/cspell-bundled-dicts" "8.8.4" - "@cspell/cspell-pipe" "8.8.4" - "@cspell/cspell-resolver" "8.8.4" - "@cspell/cspell-types" "8.8.4" - "@cspell/dynamic-import" "8.8.4" - "@cspell/strong-weak-map" "8.8.4" +cspell-grammar@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-8.13.3.tgz#147e97a54ad2c8e4925bd84ae2afed7ff14960e2" + integrity sha512-xPSgKk9HY5EsI8lkMPC9hiZCeAUs+RY/IVliUBW1xEicAJhP4RZIGRdIwtDNNJGwKfNXazjqYhcS4LS0q7xPAQ== + dependencies: + "@cspell/cspell-pipe" "8.13.3" + "@cspell/cspell-types" "8.13.3" + +cspell-io@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-8.13.3.tgz#9f2323aa7ea6edce84c16964c95ee4c83b5b880e" + integrity sha512-AeMIkz7+4VuJaPKO/v1pUpyUSOOTyLOAfzeTRRAXEt+KRKOUe36MyUmBMza6gzNcX2yD04VgJukRL408TY9ntw== + dependencies: + "@cspell/cspell-service-bus" "8.13.3" + "@cspell/url" "8.13.3" + +cspell-lib@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-8.13.3.tgz#4d2516bbb09148079d634f73c65ffa05a62ae02c" + integrity sha512-aEqxIILeqDtNoCa47/oSl5c926b50ue3PobYs4usn0Ymf0434RopCP+DCGsF7BPtog4j4XWnEmvkcJs57DYWDg== + dependencies: + "@cspell/cspell-bundled-dicts" "8.13.3" + "@cspell/cspell-pipe" "8.13.3" + "@cspell/cspell-resolver" "8.13.3" + "@cspell/cspell-types" "8.13.3" + "@cspell/dynamic-import" "8.13.3" + "@cspell/strong-weak-map" "8.13.3" + "@cspell/url" "8.13.3" clear-module "^4.1.2" - comment-json "^4.2.3" - cspell-config-lib "8.8.4" - cspell-dictionary "8.8.4" - cspell-glob "8.8.4" - cspell-grammar "8.8.4" - cspell-io "8.8.4" - cspell-trie-lib "8.8.4" + comment-json "^4.2.5" + cspell-config-lib "8.13.3" + cspell-dictionary "8.13.3" + cspell-glob "8.13.3" + cspell-grammar "8.13.3" + cspell-io "8.13.3" + cspell-trie-lib "8.13.3" env-paths "^3.0.0" fast-equals "^5.0.1" gensequence "^7.0.0" import-fresh "^3.3.0" resolve-from "^5.0.0" - vscode-languageserver-textdocument "^1.0.11" + vscode-languageserver-textdocument "^1.0.12" vscode-uri "^3.0.8" xdg-basedir "^5.1.0" -cspell-trie-lib@8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-8.8.4.tgz#99cc2a733cda3816646b2e7793bde581f9205f8b" - integrity sha512-yCld4ZL+pFa5DL+Arfvmkv3cCQUOfdRlxElOzdkRZqWyO6h/UmO8xZb21ixVYHiqhJGZmwc3BG9Xuw4go+RLig== +cspell-trie-lib@8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-8.13.3.tgz#3fe376106faef310c3874685fc6e2ddaa2551066" + integrity sha512-Z0iLGi9HI+Vf+WhVVeru6dYgQdtaYCKWRlc1SayLfAZhw9BcjrXL8KTXDfAfv/lUgnRu6xwP1isLlDNZECsKVQ== dependencies: - "@cspell/cspell-pipe" "8.8.4" - "@cspell/cspell-types" "8.8.4" + "@cspell/cspell-pipe" "8.13.3" + "@cspell/cspell-types" "8.13.3" gensequence "^7.0.0" -cspell@^8.8.4: - version "8.8.4" - resolved "https://registry.yarnpkg.com/cspell/-/cspell-8.8.4.tgz#7881d8e400c33a180ba01447c0413348a2e835d3" - integrity sha512-eRUHiXvh4iRapw3lqE1nGOEAyYVfa/0lgK/e34SpcM/ECm4QuvbfY7Yl0ozCbiYywecog0RVbeJJUEYJTN5/Mg== +cspell@^8.13.3: + version "8.13.3" + resolved "https://registry.yarnpkg.com/cspell/-/cspell-8.13.3.tgz#cebe77faeed2d6d3e0a6dc244d5c1c3fad5b3ad5" + integrity sha512-2wv4Eby7g8wDB553fI8IoZjyitoKrD2kmtdeoYUN2EjVs3RMpIOver3fL+0VaFAaN0uLfAoeAAIB5xJEakvZYQ== dependencies: - "@cspell/cspell-json-reporter" "8.8.4" - "@cspell/cspell-pipe" "8.8.4" - "@cspell/cspell-types" "8.8.4" - "@cspell/dynamic-import" "8.8.4" + "@cspell/cspell-json-reporter" "8.13.3" + "@cspell/cspell-pipe" "8.13.3" + "@cspell/cspell-types" "8.13.3" + "@cspell/dynamic-import" "8.13.3" + "@cspell/url" "8.13.3" chalk "^5.3.0" chalk-template "^1.1.0" commander "^12.1.0" - cspell-gitignore "8.8.4" - cspell-glob "8.8.4" - cspell-io "8.8.4" - cspell-lib "8.8.4" + cspell-dictionary "8.13.3" + cspell-gitignore "8.13.3" + cspell-glob "8.13.3" + cspell-io "8.13.3" + cspell-lib "8.13.3" fast-glob "^3.3.2" fast-json-stable-stringify "^2.1.0" - file-entry-cache "^8.0.0" + file-entry-cache "^9.0.0" get-stdin "^9.0.0" - semver "^7.6.2" + semver "^7.6.3" strip-ansi "^7.1.0" - vscode-uri "^3.0.8" dargs@^8.0.0: version "8.1.0" @@ -5048,10 +5237,10 @@ debug@4, debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debu dependencies: ms "2.1.2" -debug@4.3.5, debug@~4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== +debug@4.3.6, debug@~4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== dependencies: ms "2.1.2" @@ -5077,20 +5266,25 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-data-property@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz" - integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - get-intrinsic "^1.2.1" + es-define-property "^1.0.0" + es-errors "^1.3.0" gopd "^1.0.1" - has-property-descriptors "^1.0.0" delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" @@ -5106,6 +5300,11 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -5180,6 +5379,13 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.4.172: version "1.4.182" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.182.tgz" @@ -5230,10 +5436,10 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== +enhanced-resolve@^5.17.0: + version "5.17.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" + integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -5248,6 +5454,11 @@ env-paths@^3.0.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da" integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -5255,6 +5466,18 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz" @@ -5285,11 +5508,6 @@ escape-string-regexp@^4.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - eslint-config-prettier@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" @@ -5303,10 +5521,10 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" - integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== +eslint-scope@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.2.tgz#5cbb33d4384c9136083a71190d548158fe128f94" + integrity sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -5326,16 +5544,16 @@ eslint-visitor-keys@^4.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== -eslint@^9.5.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.5.0.tgz#11856034b94a9e1a02cfcc7e96a9f0956963cd2f" - integrity sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw== +eslint@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.0.tgz#8d214e69ae4debeca7ae97daebbefe462072d975" + integrity sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/config-array" "^0.16.0" + "@eslint-community/regexpp" "^4.11.0" + "@eslint/config-array" "^0.17.1" "@eslint/eslintrc" "^3.1.0" - "@eslint/js" "9.5.0" + "@eslint/js" "9.9.0" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.3.0" "@nodelib/fs.walk" "^1.2.8" @@ -5344,9 +5562,9 @@ eslint@^9.5.0: cross-spawn "^7.0.2" debug "^4.3.2" escape-string-regexp "^4.0.0" - eslint-scope "^8.0.1" + eslint-scope "^8.0.2" eslint-visitor-keys "^4.0.0" - espree "^10.0.1" + espree "^10.1.0" esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -5375,6 +5593,15 @@ espree@^10.0.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.0.0" +espree@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56" + integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA== + dependencies: + acorn "^8.12.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" @@ -5555,6 +5782,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-equals@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz" @@ -5597,10 +5829,10 @@ fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-xml-parser@4.2.5: - version "4.2.5" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz" - integrity sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g== +fast-xml-parser@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== dependencies: strnum "^1.0.5" @@ -5618,21 +5850,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -figures@^3.0.0: +figures@^3.0.0, figures@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" -figures@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz" - integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== - dependencies: - escape-string-regexp "^5.0.0" - is-unicode-supported "^1.2.0" - file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" @@ -5640,6 +5864,20 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" +file-entry-cache@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-9.0.0.tgz#4478e7ceaa5191fa9676a2daa7030211c31b1e7e" + integrity sha512-6MgEugi8p2tiUhqO7GnPsmbCCzj0YRCwwaTbpGRyKZesjRSzkqkAE9fPp7V2yMs5hwfgbQLgdvSSkGNg1s5Uvw== + dependencies: + flat-cache "^5.0.0" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -5706,11 +5944,24 @@ flat-cache@^4.0.0: keyv "^4.5.4" rimraf "^5.0.5" +flat-cache@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-5.0.0.tgz#26c4da7b0f288b408bb2b506b2cb66c240ddf062" + integrity sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ== + dependencies: + flatted "^3.3.1" + keyv "^4.5.4" + flatted@^3.2.9: version "3.2.9" resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +flatted@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -5827,6 +6078,11 @@ gcp-metadata@^6.1.0: gaxios "^6.0.0" json-bigint "^1.0.0" +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensequence@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz" @@ -5847,7 +6103,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2: version "1.1.2" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz" integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== @@ -5865,15 +6121,16 @@ get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" -get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-package-type@^0.1.0: version "0.1.0" @@ -5930,7 +6187,19 @@ glob-to-regexp@^0.4.1: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@10.3.10, glob@^10.3.7: +glob@10.4.2: + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^10.3.7: version "10.3.10" resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== @@ -5941,7 +6210,7 @@ glob@10.3.10, glob@^10.3.7: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4, glob@~7.2.0: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5953,16 +6222,6 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.2.1" - resolved "https://registry.npmjs.org/glob/-/glob-9.2.1.tgz" - integrity sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA== - dependencies: - fs.realpath "^1.0.0" - minimatch "^7.4.1" - minipass "^4.2.4" - path-scurry "^1.6.1" - global-directory@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz" @@ -5992,10 +6251,10 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -google-auth-library@^9.11.0: - version "9.11.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.11.0.tgz#bd6da364bcde4e0cc4ed70a0e0df5112b6a671dd" - integrity sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw== +google-auth-library@^9.13.0: + version "9.13.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.13.0.tgz#0735ecdc33d350699dbf3ff48601a856911bbcff" + integrity sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA== dependencies: base64-js "^1.3.0" ecdsa-sig-formatter "^1.0.11" @@ -6016,6 +6275,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" @@ -6056,12 +6320,12 @@ has-own-prop@^2.0.0: resolved "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz" integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" has-proto@^1.0.1: version "1.0.1" @@ -6131,10 +6395,10 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^9.0.11: - version "9.0.11" - resolved "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz" - integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== +husky@^9.1.4: + version "9.1.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.4.tgz#926fd19c18d345add5eab0a42b2b6d9a80259b34" + integrity sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA== iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" @@ -6166,30 +6430,40 @@ import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b" - integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw== +import-in-the-middle@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz#3e111ff79c639d0bde459bd7ba29dd9fdf357364" + integrity sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg== dependencies: acorn "^8.8.2" acorn-import-assertions "^1.9.0" cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" -import-in-the-middle@1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.4.tgz#508da6e91cfa84f210dcdb6c0a91ab0c9e8b3ebc" - integrity sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg== +import-in-the-middle@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz#c94d88d53701de9a248f9710b41f533e67f598a4" + integrity sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5" cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" -import-in-the-middle@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz#c94d88d53701de9a248f9710b41f533e67f598a4" - integrity sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ== +import-in-the-middle@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" + integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== + dependencies: + acorn "^8.8.2" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + +import-in-the-middle@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.8.1.tgz#8b51c2cc631b64e53e958d7048d2d9463ce628f8" + integrity sha512-yhRwoHtiLGvmSozNOALgjRPFI6uYsds60EoMqqnXyyv+JOIW/BrrLejuTGBt+bq0T5tLzOHrN0T7xYTm4Qt/ng== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5" @@ -6258,18 +6532,18 @@ inquirer@8.2.6: through "^2.3.6" wrap-ansi "^6.0.1" -inquirer@9.2.12: - version "9.2.12" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz" - integrity sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q== +inquirer@9.2.15: + version "9.2.15" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.15.tgz#2135a36190a6e5c92f5d205e0af1fea36b9d3492" + integrity sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg== dependencies: - "@ljharb/through" "^2.3.11" + "@ljharb/through" "^2.3.12" ansi-escapes "^4.3.2" chalk "^5.3.0" cli-cursor "^3.1.0" cli-width "^4.1.0" external-editor "^3.1.0" - figures "^5.0.0" + figures "^3.2.0" lodash "^4.17.21" mute-stream "1.0.0" ora "^5.4.1" @@ -6279,10 +6553,20 @@ inquirer@9.2.12: strip-ansi "^6.0.1" wrap-ansi "^6.2.0" -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +ioredis@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.1.tgz#1c56b70b759f01465913887375ed809134296f40" + integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" ipaddr.js@1.9.1: version "1.9.1" @@ -6396,11 +6680,6 @@ is-unicode-supported@^0.1.0: resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-unicode-supported@^1.2.0: - version "1.3.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -6483,6 +6762,25 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.8.5: + version "10.9.1" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.1.tgz#8dc96b7fcc41cb19aa502af506da4e1d56f5e62b" + integrity sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" @@ -7001,6 +7299,11 @@ jsonc-parser@3.2.1: resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +jsonc-parser@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" @@ -7122,10 +7425,10 @@ libphonenumber-js@^1.10.53: resolved "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.54.tgz" integrity sha512-P+38dUgJsmh0gzoRDoM4F5jLbyfztkU6PY6eSK6S5HwTi/LPvnwXqVCQZlAy1FxZ5c48q25QhxGQ0pq+WQcSlQ== -lilconfig@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" - integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== +lilconfig@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== limiter@^1.1.5: version "1.1.5" @@ -7137,32 +7440,32 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^15.2.7: - version "15.2.7" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" - integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== +lint-staged@^15.2.9: + version "15.2.9" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.9.tgz#bf70d40b6b192df6ad756fb89822211615e0f4da" + integrity sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ== dependencies: chalk "~5.3.0" commander "~12.1.0" - debug "~4.3.4" + debug "~4.3.6" execa "~8.0.1" - lilconfig "~3.1.1" - listr2 "~8.2.1" + lilconfig "~3.1.2" + listr2 "~8.2.4" micromatch "~4.0.7" pidtree "~0.6.0" string-argv "~0.3.2" - yaml "~2.4.2" + yaml "~2.5.0" -listr2@~8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.1.tgz#06a1a6efe85f23c5324180d7c1ddbd96b5eefd6d" - integrity sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g== +listr2@~8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" + integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20" eventemitter3 "^5.0.1" - log-update "^6.0.0" - rfdc "^1.3.1" + log-update "^6.1.0" + rfdc "^1.4.1" wrap-ansi "^9.0.0" loader-runner@^4.2.0: @@ -7206,6 +7509,11 @@ lodash.compact@^3.0.1: resolved "https://registry.npmjs.org/lodash.compact/-/lodash.compact-3.0.1.tgz" integrity sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz" @@ -7216,6 +7524,11 @@ lodash.includes@^4.3.0: resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" @@ -7299,14 +7612,14 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-update@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" - integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== dependencies: - ansi-escapes "^6.2.0" - cli-cursor "^4.0.0" - slice-ansi "^7.0.0" + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" strip-ansi "^7.1.0" wrap-ansi "^9.0.0" @@ -7317,10 +7630,10 @@ lru-cache@6.0.0, lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.14.1: - version "7.18.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.1.tgz" - integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== +lru-cache@^10.2.0, lru-cache@^10.2.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== "lru-cache@^9.1.1 || ^10.0.0": version "10.0.1" @@ -7335,10 +7648,10 @@ lru-memoizer@^2.2.0: lodash.clonedeep "^4.5.0" lru-cache "6.0.0" -luxon@~3.4.0: - version "3.4.3" - resolved "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz" - integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg== +luxon@^3.2.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" + integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== magic-string@0.30.0: version "0.30.0" @@ -7347,10 +7660,10 @@ magic-string@0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@0.30.5: - version "0.30.5" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz" - integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== +magic-string@0.30.8: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -7463,17 +7776,22 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^7.4.1: - version "7.4.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz" - integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA== +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" @@ -7501,16 +7819,16 @@ minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minipass@^4.0.2, minipass@^4.2.4: - version "4.2.4" - resolved "https://registry.npmjs.org/minipass/-/minipass-4.2.4.tgz" - integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ== - "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" @@ -7553,23 +7871,23 @@ mongodb-connection-string-url@^3.0.0: "@types/whatwg-url" "^11.0.2" whatwg-url "^13.0.0" -mongodb@6.6.2: - version "6.6.2" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.6.2.tgz#7ecdd788e9162f6c5726cef40bdd2813cc01e56c" - integrity sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw== +mongodb@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.7.0.tgz#f86e51e6530e6a2ca4a99d7cfdf6f409223ac199" + integrity sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA== dependencies: "@mongodb-js/saslprep" "^1.1.5" bson "^6.7.0" mongodb-connection-string-url "^3.0.0" -mongoose@^8.4.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.4.1.tgz#21891c0b72890b33c7bb082b69a32f627b7043bf" - integrity sha512-odQ2WEWGL3hb0Qex+QMN4eH6D34WdMEw7F1If2MGABApSDmG9cMmqv/G1H6WsXmuaH9mkuuadW/WbLE5+tHJwA== +mongoose@^8.5.2: + version "8.5.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.5.2.tgz#73b40ce778f3fc66407aba3c3157795cdd278543" + integrity sha512-GZB4rHMdYfGatV+23IpCrqFbyCOjCNOHXgWbirr92KRwTEncBrtW3kgU9vmpKjsGf7nMmnAy06SwWUv1vhDkSg== dependencies: bson "^6.7.0" kareem "2.6.3" - mongodb "6.6.2" + mongodb "6.7.0" mpath "0.9.0" mquery "5.0.0" ms "2.1.3" @@ -7602,6 +7920,27 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012" + integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA== + dependencies: + node-gyp-build-optional-packages "5.2.2" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3" + +msgpackr@^1.10.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.0.tgz#8321d52333048cadc749f56385e3231e65337091" + integrity sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw== + optionalDependencies: + msgpackr-extract "^3.0.2" + multer@1.4.4-lts.1: version "1.4.4-lts.1" resolved "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz" @@ -7670,6 +8009,11 @@ node-abort-controller@^3.0.1: resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz" integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== +node-abort-controller@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + node-emoji@1.11.0: version "1.11.0" resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz" @@ -7691,6 +8035,13 @@ node-fetch@^2.6.9: dependencies: whatwg-url "^5.0.0" +node-gyp-build-optional-packages@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" + integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw== + dependencies: + detect-libc "^2.0.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -7780,13 +8131,19 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -opentelemetry-instrumentation-fetch-node@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.0.tgz#5beaad33b622f7021c61733af864fb505cd35626" - integrity sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA== +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== dependencies: - "@opentelemetry/api" "^1.6.0" - "@opentelemetry/instrumentation" "^0.43.0" + mimic-function "^5.0.0" + +opentelemetry-instrumentation-fetch-node@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.3.tgz#beb24048bdccb1943ba2a5bbadca68020e448ea7" + integrity sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A== + dependencies: + "@opentelemetry/instrumentation" "^0.46.0" "@opentelemetry/semantic-conventions" "^1.17.0" optionator@^0.9.3: @@ -7873,6 +8230,11 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -7980,13 +8342,13 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-scurry@^1.6.1: - version "1.6.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.1.tgz" - integrity sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^7.14.1" - minipass "^4.0.2" + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.7: version "0.1.7" @@ -8052,10 +8414,10 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz" - integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== +picomatch@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" + integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" @@ -8138,10 +8500,10 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-format@^29.0.0, pretty-format@^29.0.1: version "29.0.1" @@ -8166,7 +8528,12 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -prompts@^2.0.1: +promise-coalesce@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/promise-coalesce/-/promise-coalesce-1.1.2.tgz#5d3bc4d0b2cf2e41e9df7cbeb6519b2a09459e3d" + integrity sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg== + +prompts@^2.0.1, prompts@~2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -8278,12 +8645,29 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== dependencies: - resolve "^1.1.6" + redis-errors "^1.0.0" + +redis@^4.3.1: + version "4.7.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.7.0.tgz#b401787514d25dd0cfc22406d767937ba3be55d6" + integrity sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ== + dependencies: + "@redis/bloom" "1.2.0" + "@redis/client" "1.6.0" + "@redis/graph" "1.1.1" + "@redis/json" "1.0.7" + "@redis/search" "1.2.0" + "@redis/time-series" "1.1.0" reflect-metadata@^0.2.2: version "0.2.2" @@ -8336,7 +8720,7 @@ resolve.exports@^2.0.0: resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.0.tgz" integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== -resolve@^1.1.6, resolve@^1.20.0: +resolve@^1.20.0: version "1.22.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -8370,30 +8754,23 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -restore-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" + onetime "^7.0.0" + signal-exit "^4.1.0" reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" - integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== - -rimraf@4.4.1: - version "4.4.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz" - integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== - dependencies: - glob "^9.2.0" +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rimraf@^5.0.5: version "5.0.5" @@ -8419,6 +8796,27 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs-marbles@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/rxjs-marbles/-/rxjs-marbles-7.0.1.tgz#92e99366a26497197f0c701ff944dc46ba876487" + integrity sha512-QBJcNSml6uqx1BunqQOA2Nwh8jSFwbLF+w7b13WVC/XkheTjGrqzR6GvkES1+7DEjH58nVi4qO9KxKIofgngQQ== + dependencies: + fast-equals "^2.0.0" + rxjs-report-usage "^1.0.4" + +rxjs-report-usage@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/rxjs-report-usage/-/rxjs-report-usage-1.0.6.tgz#6e06034d9e1592e8a45bee877631638e4bac2576" + integrity sha512-omv1DIv5z1kV+zDAEjaDjWSkx8w5TbFp5NZoPwUipwzYVcor/4So9ZU3bUyQ1c8lxY5Q0Es/ztWW7PGjY7to0Q== + dependencies: + "@babel/parser" "^7.10.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" + bent "~7.3.6" + chalk "~4.1.0" + glob "~7.2.0" + prompts "~2.4.2" + rxjs@7.8.1, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" @@ -8490,7 +8888,7 @@ semver@^7.3.8: dependencies: lru-cache "^6.0.0" -semver@^7.5.2, semver@^7.6.2: +semver@^7.5.2: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -8509,6 +8907,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" @@ -8545,15 +8948,17 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" -set-function-length@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz" - integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - define-data-property "^1.1.1" - get-intrinsic "^1.2.1" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" gopd "^1.0.1" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.2" setprototypeof@1.2.0: version "1.2.0" @@ -8584,15 +8989,6 @@ shebang-regex@^3.0.0: resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@0.8.5: - version "0.8.5" - resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shimmer@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" @@ -8645,7 +9041,7 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -slice-ansi@^7.0.0: +slice-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== @@ -8661,7 +9057,7 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@0.5.21, source-map-support@~0.5.20: +source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -8710,17 +9106,22 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -stop-only@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/stop-only/-/stop-only-3.3.2.tgz#cea846c42bd8c1a19f07209d724aacc549116859" - integrity sha512-5LnzJyC0XIJA/7IPUxk3O2l6onc0ui0pp3dVbuQVgcr2iGgsh2uZmtrYCUJkYbLdILSY3NxdLYV0h90lIbALdg== +stop-only@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/stop-only/-/stop-only-3.3.3.tgz#c259b8746f2058ce7bce9282ec47e49ba4a9afe8" + integrity sha512-DVvoYzfqucHGkJ8pEK8p7RRD41gJDPBkPIsiIrS8Ex7ncXhfwO3ddWfmUhbEG4CYE766tZKPvV5d5AkTB6DHEQ== dependencies: - debug "4.3.5" + debug "4.3.6" execa "0.11.0" minimist "1.2.8" @@ -8897,10 +9298,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swagger-ui-dist@5.11.2: - version "5.11.2" - resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz" - integrity sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A== +swagger-ui-dist@5.17.14: + version "5.17.14" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz#e2c222e5bf9e15ccf80ec4bc08b4aaac09792fd6" + integrity sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw== symbol-observable@4.0.0: version "4.0.0" @@ -9018,12 +9419,13 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-jest@^29.1.5: - version "29.1.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.5.tgz#d6c0471cc78bffa2cb4664a0a6741ef36cfe8f69" - integrity sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg== +ts-jest@^29.2.4: + version "29.2.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.4.tgz#38ccf487407d7a63054a72689f6f99b075e296e5" + integrity sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw== dependencies: bs-logger "0.x" + ejs "^3.1.10" fast-json-stable-stringify "2.x" jest-util "^29.0.0" json5 "^2.2.3" @@ -9114,6 +9516,11 @@ tslib@2.6.2, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@2.6.3, tslib@^2.0.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^2.1.0, tslib@^2.3.1: version "2.4.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" @@ -9154,24 +9561,24 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.13.0.tgz#bfd6f139b61e12d171af8621869785cb3b29f1b7" - integrity sha512-upO0AXxyBwJ4BbiC6CRgAJKtGYha2zw4m1g7TIVPSonwYEuf7vCicw3syjS1OxdDMTz96sZIXl3Jx3vWJLLKFw== +typescript-eslint@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.1.0.tgz#c43a3543ab34c37b7f88deb4ff18b9764aed0b60" + integrity sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow== dependencies: - "@typescript-eslint/eslint-plugin" "7.13.0" - "@typescript-eslint/parser" "7.13.0" - "@typescript-eslint/utils" "7.13.0" + "@typescript-eslint/eslint-plugin" "8.1.0" + "@typescript-eslint/parser" "8.1.0" + "@typescript-eslint/utils" "8.1.0" typescript@5.3.3: version "5.3.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -typescript@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== uid@2.0.2: version "2.0.2" @@ -9180,10 +9587,10 @@ uid@2.0.2: dependencies: "@lukeed/csprng" "^1.0.0" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5" + integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg== unicorn-magic@^0.1.0: version "0.1.0" @@ -9233,7 +9640,7 @@ utils-merge@1.0.1, utils-merge@^1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@9.0.1, uuid@^9.0.1: +uuid@9.0.1, uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -9275,10 +9682,10 @@ verify-apple-id-token@^3.1.2: jsonwebtoken "^9.0.2" jwks-rsa "^3.1.0" -vscode-languageserver-textdocument@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz" - integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== +vscode-languageserver-textdocument@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== vscode-uri@^3.0.8: version "3.0.8" @@ -9292,10 +9699,10 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -9327,26 +9734,26 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.90.1: - version "5.90.1" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz" - integrity sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog== +webpack@5.93.0: + version "5.93.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5" + integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" + acorn-import-attributes "^1.9.5" browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" + enhanced-resolve "^5.17.0" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" @@ -9354,7 +9761,7 @@ webpack@5.90.1: schema-utils "^3.2.0" tapable "^2.1.1" terser-webpack-plugin "^5.3.10" - watchpack "^2.4.0" + watchpack "^2.4.1" webpack-sources "^3.2.3" whatwg-url@^13.0.0: @@ -9491,7 +9898,7 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^4.0.0: +yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== @@ -9501,10 +9908,10 @@ yaml@^1.10.0: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.4.3, yaml@~2.4.2: - version "2.4.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.3.tgz#0777516b8c7880bcaa0f426a5410e8d6b0be1f3d" - integrity sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg== +yaml@^2.5.0, yaml@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" + integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== yargs-parser@21.1.1, yargs-parser@^21.1.1: version "21.1.1"