diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aba9ac8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +#husky + +# Build dependencies +node_modules/ +coverage/ +coverage-e2e/ +coverage-integration/ +dist/ +.husky/ +.github/ +prod/ + +# Logs +logs/ + +# Environment (contains sensitive data) +.env* + +# Versioning and metadata +.git +.gitignore +.dockerignore +.eslintignore + +# Files not required for production +.editorconfig +dockerfile +docker-compose.yml +cspell.json +README.md +nodemon.json \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e0a6456 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +APP_NAME=NEST +APP_ENV=development +APP_LANGUAGE=en + +HTTP_ENABLE=true +HTTP_HOST=localhost +HTTP_PORT= 3000 +HTTP_VERSIONING_ENABLE=true +HTTP_VERSION=1 + +DEBUGGER_HTTP_WRITE_INTO_FILE=false +DEBUGGER_HTTP_WRITE_INTO_CONSOLE=false +DEBUGGER_SYSTEM_WRITE_INTO_FILE=false +DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE=false + +JOB_ENABLE=false + +DATABASE_HOST=mongodb://localhost:30001,localhost:30002,localhost:30003 +DATABASE_NAME=nest +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_DEBUG=false +DATABASE_OPTIONS=replicaSet=rs0&retryWrites=true&w=majority + +AUTH_JWT_SUBJECT=nestDevelopment +AUTH_JWT_ISSUER=nest +AUTH_JWT_AUDIENCE=https://example.com + +AUTH_JWT_ACCESS_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT +AUTH_JWT_ACCESS_TOKEN_EXPIRED=15m + +AUTH_JWT_REFRESH_TOKEN_SECRET_KEY=7Y3nqaO8jKVOFBRy9ujn5uUxV8Iy2otHrnQgiXlIGAqiVdb5 +AUTH_JWT_REFRESH_TOKEN_EXPIRED=7d +AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED=30d +AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION=15m + +AUTH_PERMISSION_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT +AUTH_PERMISSION_TOKEN_EXPIRED=5m + +AUTH_JWT_PAYLOAD_ENCRYPT=false +AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY=fKyRq7g9eftVNEdiCC7lNU6fga5Pr1iC7dc0JYsC +AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV=mLZZdQrXqjPW5F5H2eko +AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY=NnCnSrRmw5YuQyTPtDokOWmKR37EYbuB6ITZqqZd +AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV=eP7P8Pmvq207zhyd61dz + +AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY=hUcRIUQzJMe17w8cAZAreMdjxjo1JbucBACu7tAw +AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV=7V0D5a0D3SdsgM1KT5rF + +AWS_CREDENTIAL_KEY= +AWS_CREDENTIAL_SECRET= +AWS_S3_REGION=ap-southeast-3 +AWS_S3_BUCKET=baibay-development \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0502376 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,9 @@ +# /node_modules/* in the project root is ignored by default +# build artefacts +dist/* +coverage/* +node_modules/* +logs/* +prod/* +.husky/* +.github/* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a951aaa --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "tsconfigRootDir": ".", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint/eslint-plugin"], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "root": true, + "env": { + "node": true, + "jest": true + }, + "ignorePatterns": [".eslintrc.js"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9f73384 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + 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/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..bfe6046 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,31 @@ +name: Linter +on: + + workflow_dispatch: + pull_request: + branches: + - master + - development + +jobs: + linter: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: ['18.x'] + + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Setup node version ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: yarn --frozen-lockfile + + - name: Linter + run: yarn lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ded86a1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Release + +permissions: + contents: write + +on: + + # push: + # branches: + # - main + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - 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: martinbeentjes/npm-get-version-action@main + + - name: Git + run: | + echo Branch name is: ${{ github.ref_name }} + echo Short sha: ${{ steps.git.outputs.short_sha }} + echo Version is: ${{ steps.version.outputs.current-version }} + + - name: Release + uses: softprops/action-gh-release@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.current-version }} + name: v${{ steps.version.outputs.current-version }} + generate_release_notes: true + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index cab85ca..95b81ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,53 @@ +# compiled output +/dist /node_modules -/coverage +.warmup/ + +# Logs +logs *.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS .DS_Store + +# Tests +/coverage +/coverage-e2e +/coverage-integration +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# yarn +.yarn/* +.pnp.* +!.yarn/cache +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# environment +.env* +!.env.example +config.yaml +config.yml \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..15e0bfa --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint +yarn deadcode +yarn spell diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 4c9adda..0000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -./docs -./scripts -./tests -./incoming \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2d6310f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 4 +} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b0e20f5..0000000 --- a/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d152665 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) [2023] [Fedi DAYEG] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index dc27f0f..c5a8af0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,514 @@ -# osr-package-template +[![Contributors][nest-contributors-shield]][nest-contributors] +[![Forks][nest-forks-shield]][nest-forks] +[![Stargazers][nest-stars-shield]][nest-stars] +[![Issues][nest-issues-shield]][nest-issues] +[![MIT License][nest-license-shield]][license] -Package basics \ No newline at end of file +[![NestJs][nestjs-shield]][ref-nestjs] +[![NodeJs][nodejs-shield]][ref-nodejs] +[![Typescript][typescript-shield]][ref-typescript] +[![MongoDB][mongodb-shield]][ref-mongodb] +[![JWT][jwt-shield]][ref-jwt] +[![Jest][jest-shield]][ref-jest] +[![Yarn][yarn-shield]][ref-yarn] +[![Docker][docker-shield]][ref-docker] + +# Complete NestJs Boilerplate πŸ”₯ πŸš€ + +> This repo will representative of authentication service and authorization service + +[Complete NestJs][nest] is a [Http NestJs v9.x][ref-nestjs] boilerplate. Best uses for backend service. + +*You can [request feature][nest-issues] or [report bug][nest-issues] with following this link* + +## Table of contents + +* [Important](#important) +* [Next Todo](#next-todo) +* [Build With](#build-with) +* [Objective](#objective) +* [Features](#features) +* [Structure](#structure) + * [Folder Structure](#folder-structure) + * [Module Structure](#module-structure) + * [Response Structure](#response-structure) +* [Prerequisites](#prerequisites) +* [Getting Started](#getting-started) + * [Clone Repo](#clone-repo) + * [Install Dependencies](#install-dependencies) + * [Create environment](#create-environment) + * [Database Migration](#database-migration) + * [Test](#test) + * [Run Project](#run-project) + * [Run Project with Docker](#run-project-with-docker) +* [API Reference](#api-reference) +* [Environment](#environment) +* [Api Key Encryption](#api-key-encryption) +* [Adjust Mongoose Setting](#adjust-mongoose-setting) +* [License](#license) +* [Contact](#contact) + +## Important + +* The features will replated with AWS Features +* If you want to implementΒ `database transactions`, you must run MongoDB as aΒ `replication set`. +* If you change the environment value of `APP_ENV` to `production`, that will trigger. + 1. CorsMiddleware will implement `src/configs/middleware.config.ts`. + 2. Documentation will `disable`. + +## Next Todo + +Next development + +* [x] serialization update +* [x] update unit test for common modules +* [x] user e2e +* [x] api key for saas, change `x-api-key` to `${key}:${secret}` +* [ ] user plan module +* [ ] invoicing plan module +* [ ] excel decorator optimize +* [ ] Authorization optimize, remove permission entity and implement policy guard +* [ ] Google SSO +* [ ] Background export/import from/to CSV and Excel +* [ ] Update Documentation, include an diagram for easier comprehension + +## Build with + +Describes which version. + +| Name | Version | +| ---------- | -------- | +| NestJs | v9.3.x | +| NodeJs | v18.12.x | +| Typescript | v5.0.x | +| Mongoose | v7.0.x | +| MongoDB | v6.0.x | +| Yarn | v1.22.x | +| NPM | v8.19.x | +| Docker | v20.10.x | +| Docker Compose | v2.6.x | +| Swagger | v6.2.x | + +## Objective + +* Easy to maintenance +* NestJs Habit +* Component based folder structure +* Stateless authentication and authorization +* Repository Design Pattern or Data Access Layer Design Pattern +* Follow Community Guide Line +* Follow The Twelve-Factor App +* Adopt SOLID and KISS principle +* Support Microservice Architecture, Serverless Architecture, Clean Architecture, and/or Hexagonal Architecture + +## Features + +### Main Features + +* NestJs v9.x πŸ₯³ +* Typescript πŸš€ +* Production ready πŸ”₯ +* Repository Design Pattern (Multi Repository, can mix with `TypeORM`) +* Swagger / OpenAPI 3 included +* Authentication (`Access Token`, `Refresh Token`, `API Key`, and `Google SSO`) +* Authorization, Role and Permission Management (`PermissionToken`) +* 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` +* Url Versioning, default version is `1` +* Server Side Pagination +* Import and export data with CSV or Excel by using `decorator` + +### Database + +* MongoDB integrate by using [mongoose][ref-mongoose] πŸŽ‰ +* Multi Database +* Database Transaction +* Database Soft Delete +* Database Migration + +### Logger and Debugger + +* Logger with `Morgan` +* Debugger with `Winston` πŸ“ + +### Security + +* Apply `helmet`, `cors`, and `rate-limit` +* Timeout awareness and can override βŒ›οΈ +* User agent awareness, and can whitelist user agent + +### Setting + +* Support environment file +* Centralize configuration πŸ€– +* Centralize response +* Centralize exception filter +* Setting from database πŸ—Ώ + +### Third Party Integration + +* Storage integration with `AwsS3` +* Upload file `single` and `multipart` to AwsS3 + +### Others + +* Support Docker installation +* Support CI/CD with Github Action or Jenkins +* Husky GitHook for check source code, and run test before commit 🐢 +* Linter with EsLint for Typescript + +## Structure + +### Folder Structure + +1. `/app` The final wrapper module +2. `/common` The common module +3. `/configs` The configurations for this project +4. `/health` health check module for every service integrated +5. `/jobs` cron job or schedule task +6. `/language` json languages +7. `/migration` migrate all init data +8. `/modules` other modules based on service based on project +9. `/router` endpoint router. `Controller` will put in this + +### Module structure + +Full structure of module + +```txt +. +└── module1 + β”œβ”€β”€ abstracts + β”œβ”€β”€ constants // constant like enum, static value, status code, etc + β”œβ”€β”€ controllers // business logic for rest api + β”œβ”€β”€ decorators // warper decorator, custom decorator, etc + β”œβ”€β”€ dtos // request validation + β”œβ”€β”€ docs // swagger / OpenAPI 3 + β”œβ”€β”€ errors // custom error + β”œβ”€β”€ filters // custom filter + β”œβ”€β”€ guards // validate related with database + β”œβ”€β”€ indicators // custom health check indicator + β”œβ”€β”€ interceptors // custom interceptors + β”œβ”€β”€ interfaces + β”œβ”€β”€ middleware + β”œβ”€β”€ pipes + β”œβ”€β”€ repository + β”œβ”€β”€ entities // database entities + β”œβ”€β”€ repositories // database repositories + └── module1.repository.module.ts + β”œβ”€β”€ serializations // response serialization + β”œβ”€β”€ services + β”œβ”€β”€ tasks // task for cron job + └── module1.module.ts +``` + +### Response Structure + +This section will describe the structure of the response. + +#### Response Default + +Default response for the response + +```ts +export class ResponseDefaultSerialization { + statusCode: number; + message: string; + _metadata?: IResponseMetadata; + data?: Record; +} +``` + +#### Response Paging + +Default response for pagination. + +```ts +export class ResponsePagingSerialization { + statusCode: number; + message: string; + totalData: number; + totalPage?: number; + currentPage?: number; + perPage?: number; + _availableSearch?: string[]; + _availableSort?: string[]; + _metadata?: IResponseMetadata; + data: Record[]; +} + +``` + +#### Response Metadata + +This is useful when we need to give the frontend some information that is related / not related with the endpoint. + +```ts +export interface IResponseMetadata { + languages: ENUM_MESSAGE_LANGUAGE[]; + timestamp: number; + timezone: string; + requestId: string; + path: string; + version: string; + repoVersion: string; + nextPage?: string; + previousPage?: string; + firstPage?: string; + lastPage?: string; + [key: string]: any; +} +``` + +## Prerequisites + +We assume that everyone who comes here is **`programmer with intermediate knowledge`** and we also need to understand more before we begin in order to reduce the knowledge gap. + +1. Understand [NestJs Fundamental][ref-nestjs], Main Framework. NodeJs Framework with support fully TypeScript. +2. Understand[Typescript Fundamental][ref-typescript], Programming Language. It will help us to write and read the code. +3. Understand [ExpressJs Fundamental][ref-nodejs], NodeJs Base Framework. It will help us in understanding how the NestJs Framework works. +4. Understand what NoSql is and how it works as a database, especially [MongoDB.][ref-mongodb] +5. Understand Repository Design Pattern or Data Access Object Design Pattern. It will help to read, and write the source code +6. Understand The SOLID Principle and KISS Principle for better write the code. +7. Optional. Understand Microservice Architecture, Clean Architecture, and/or Hexagonal Architecture. It can help to serve the project. +8. Optional. Understanding [The Twelve Factor Apps][ref-12factor]. It can help to serve the project. +9. Optional. Understanding [Docker][ref-docker]. It can help to run the project. + +## 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/fedi-dayeg/complete-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 +``` + +To know the details, you can read the documentation. [Jump to document section](#documentation) + +### 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 +``` + +### Test + +> The test is still not good net. I'm still lazy too do that. + +The project provide 3 automation testing `unit testing`, `integration testing`, and `e2e testing`. + +```bash +yarn test +``` + +For specific test do this + +* Unit testing + + ```bash + yarn test:unit + ``` + +* Integration testing + + ```bash + yarn test:integration + ``` + +* E2E testing + + ```bash + yarn test:e2e + ``` + +### 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 in our instance. + +1. [Docker][ref-docker] +2. [Docker-Compose][ref-dockercompose] + +Then run + +```bash +docker-compose up -d +``` + +## API Reference + +You can check The ApiSpec after running this project. [here][api-reference-docs] + +## Documentation + +> Ongoing update + +## Adjust Mongoose Setting + +> Optional, if your mongodb version is < 5 + +Go to file `src/common/database/services/database.options.service.ts` and add `useMongoClient` to `mongooseOptions` then set value to `true`. + +```typescript +const mongooseOptions: MongooseModuleOptions = { + uri, + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + useMongoClient: true // <--- add this +}; +``` + +## License + +Distributed under [MIT licensed][license]. + +## Contribute + +How to contribute in this repo + +1. Fork the project with click `Fork` button of this repo. +2. Clone the fork project + + ```bash + git clone "url you just copied" + ``` + +3. Make necessary changes and commit those changes +4. Commit the changes + + ```bash + git commit -m "your message" + ``` + +5. Push changes to fork project + + ```bash + git push origin -u main + ``` + +6. Back to browser, goto your fork repo github. Then, click `Compare & pull request` + +If your code behind commit with the original, please update your code and resolve the conflict. Then, repeat from number 6. + +### Rule + +* Avoid Circular Dependency +* Consume component folder structure, and repository design pattern +* Always make `service` for every module is independently. +* Do not put `controller` into service modules, cause this will break the dependency. Only put the controller into `router` and then inject the dependency. +* Put the config in `/configs` folder, and for dynamic config put as `environment variable` +* `CommonModule` only for main package, and put the module that related of service/project into `/src/modules`. So, if we want to clear the unnecessary module, we just need to delete the `src/modules/**` +* If there a new service in CommonModule. Make sure to create the unit test in `/test/unit`. +* If there a new controller, make sure to create the e2e testing in `test/e2e` + +## Contact + +[Fedi DAYEG][author-email] + +[![Github][github-shield]][author-github] +[![LinkedIn][linkedin-shield]][author-linkedin] + + +[nest-contributors-shield]: https://img.shields.io/github/contributors/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-forks-shield]: https://img.shields.io/github/forks/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-stars-shield]: https://img.shields.io/github/stars/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-issues-shield]: https://img.shields.io/github/issues/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-license-shield]: https://img.shields.io/github/license/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge + +[nestjs-shield]: https://img.shields.io/badge/nestjs-%23E0234E.svg?style=for-the-badge&logo=nestjs&logoColor=white +[nodejs-shield]: https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white +[typescript-shield]: https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white +[mongodb-shield]: https://img.shields.io/badge/MongoDB-white?style=for-the-badge&logo=mongodb&logoColor=4EA94B +[jwt-shield]: https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=JSON%20web%20tokens&logoColor=white +[jest-shield]: https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white +[yarn-shield]: https://img.shields.io/badge/yarn-%232C8EBB.svg?style=for-the-badge&logo=yarn&logoColor=white +[docker-shield]: https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white + +[github-shield]: https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white +[linkedin-shield]: https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white + + +[author-linkedin]: https://www.linkedin.com/in/fedi-dayeg-a330b6131/ +[author-email]: mailto:fedi.dayeg@gmail.com +[author-github]: https://github.com/fedi-dayeg + + +[nest-issues]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/issues +[nest-stars]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/stargazers +[nest-forks]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/network/members +[nest-contributors]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/graphs/contributors + + +[nest]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate + + + +[license]: LICENSE.md + + +[ref-nestjs]: http://nestjs.com +[ref-mongoose]: https://mongoosejs.com +[ref-mongodb]: https://docs.mongodb.com/ +[ref-nodejs]: https://nodejs.org/ +[ref-typescript]: https://www.typescriptlang.org/ +[ref-docker]: https://docs.docker.com +[ref-dockercompose]: https://docs.docker.com/compose/ +[ref-yarn]: https://yarnpkg.com +[ref-12factor]: https://12factor.net +[ref-nestjscommand]: https://gitlab.com/aa900031/nestjs-command +[ref-jwt]: https://jwt.io +[ref-jest]: https://jestjs.io/docs/getting-started +[ref-git]: https://git-scm.com + + +[api-reference-docs]: http://localhost:3000/docs diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..3b49d9b --- /dev/null +++ b/cspell.json @@ -0,0 +1,60 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "nestjs", + "metatype", + "virtuals", + "prebuild", + "logform", + "transfomer", + "insync", + "microservices", + "globby", + "dockerhub", + "fifsky", + "buildx", + "exceljs", + "milis", + "workdir", + "dbdata", + "initdb", + "deadcode", + "authapis", + "headerapikey", + "jenkinsfile", + "superadmin", + "alphanum", + "dtos", + "typeorm", + "apikeys", + "apikey", + "maxlength", + "Streamable", + "unallow", + "datauser" + ], + "ignoreWords": [ + "psheon", + "aallithioo", + "tiaamoo", + "qwertyuiop12345zxcvbnmkjh", + "opbUwdiS1FBsrDUoPgZdx", + "cuwakimacojulawu", + "baibay", + "acks", + "andrechristikan", + "trueaaa", + "aasdasd" + ], + "ignorePaths": [ + "node_modules/**", + "endpoints/**", + "*coverage/**", + ".husky/**", + ".github/**", + "dist/**", + "logs/**", + "src/database/json/**" + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..21bbf3d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' +services: + nestService: + build: . + container_name: nestService + hostname: nestService + ports: + - 3000:3000 + networks: + - app-network + volumes: + - ./src/:/app/src/ + - .env/:/app/.env + restart: unless-stopped + depends_on: + - nestDatabase + nestDatabase: + image: mongo:latest + container_name: nestDatabase + hostname: nestDatabase + ports: + - 27017:27017 + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: 123456 + MONGO_INITDB_DATABASE: nest + volumes: + - dbdata:/data/db + restart: unless-stopped + networks: + - app-network +networks: + app-network: + name: app-network + driver: bridge +volumes: + dbdata: \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..742e2bb --- /dev/null +++ b/dockerfile @@ -0,0 +1,14 @@ +FROM node:lts-alpine +LABEL maintainer "fedi.dayeg@gmail.com" + +WORKDIR /app +EXPOSE 3000 + +COPY package.json yarn.lock ./ +RUN touch .env + +RUN set -x && yarn + +COPY . . + +CMD [ "yarn", "start:dev" ] diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..e7ad649 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,16 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "plugins": ["@nestjs/swagger"], + "assets": [ + { + "include": "languages/**/*", + "outDir": "dist/src" + } + ], + "webpack": false, + "deleteOutDir": true, + "watchAssets": false + } +} \ No newline at end of file diff --git a/package.json b/package.json index e67de54..ffa5247 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,135 @@ { - "name": "@plastichub/template", - "description": "", - "version": "0.3.1", - "main": "main.js", - "typings": "index.d.ts", - "publishConfig": { - "access": "public" + "name": "nestjs-boilerplate", + "version": "1.0.0", + "description": "NestJs Boilerplate", + "repository": { + "type": "git" }, - "bin": { - "osr-bin": "main.js" + "author": { + "name": "Fedi DAYEG", + "email": "fedi.dayeg@gmail.com" + }, + "private": true, + "license": "MIT", + "scripts": { + "upgrade:package": "ncu -u", + "prebuild": "rimraf dist", + "build": "nest build", + "format": "yarn format:src && yarn format:test", + "format:src": "prettier --write src/**/*.ts", + "format:test": "prettier --write test/**/*.ts", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/src/main", + "lint": "yarn lint:src && yarn lint:test", + "lint:fix": "eslint --ext .ts,.tsx '{src,test}/**/*.ts' --fix --no-error-on-unmatched-pattern", + "lint:src": "eslint --ext .ts,.tsx 'src/**/*.ts' --no-error-on-unmatched-pattern", + "lint:test": "eslint --ext .ts,.tsx 'test/**/*.ts' --no-error-on-unmatched-pattern", + "test": "yarn test:unit && yarn test:integration && yarn test:e2e", + "test:unit": "jest --config test/unit/jest.json --passWithNoTests --forceExit", + "test:integration": "jest --config test/integration/jest.json --passWithNoTests --forceExit", + "test:e2e": "jest --runInBand --config test/e2e/jest.json --verbose --passWithNoTests --forceExit", + "prepare": "husky install", + "deadcode": "ts-prune --project tsconfig.json --skip *.json", + "deadcode:filter": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)'", + "deadcode:count": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)' | wc -l", + "spell": "yarn spell:src && yarn spell:test", + "spell:src": "cspell lint --config cspell.json src/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true", + "spell:test": "cspell lint --config cspell.json test/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true", + "seed:setting": "nestjs-command seed:setting", + "seed:apikey": "nestjs-command seed:apikey", + "seed:permission": "nestjs-command seed:permission", + "seed:role": "nestjs-command seed:role", + "seed:user": "nestjs-command seed:user", + "rollback:setting": "nestjs-command remove:setting", + "rollback:apikey": "nestjs-command remove:apikey", + "rollback:permission": "nestjs-command remove:permission", + "rollback:role": "nestjs-command remove:role", + "rollback:user": "nestjs-command remove:user", + "seed": "yarn seed:setting && yarn seed:permission && yarn seed:role && yarn seed:user && yarn seed:apikey", + "rollback": "yarn rollback:setting && yarn rollback:apikey && yarn rollback:user && yarn rollback:role && yarn rollback:permission" }, "dependencies": { - "@types/node": "^14.17.5", - "@types/yargs": "^17.0.2", - "chalk": "^2.4.1", - "convert-units": "^2.3.4", - "env-var": "^7.0.1", - "typescript": "^4.3.5", - "yargs": "^14.2.3", - "yargs-parser": "^15.0.3" + "@aws-sdk/client-s3": "^3.292.0", + "@faker-js/faker": "^7.6.0", + "@joi/date": "^2.1.0", + "@nestjs/axios": "^2.0.0", + "@nestjs/common": "^9.3.10", + "@nestjs/config": "^2.3.1", + "@nestjs/core": "^9.3.10", + "@nestjs/jwt": "^10.0.2", + "@nestjs/mongoose": "^9.2.1", + "@nestjs/passport": "^9.0.3", + "@nestjs/platform-express": "^9.3.10", + "@nestjs/schedule": "^2.2.0", + "@nestjs/swagger": "^6.2.1", + "@nestjs/terminus": "^9.2.1", + "@nestjs/throttler": "^4.0.0", + "@types/response-time": "^2.3.5", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "crypto-js": "^4.1.1", + "geolib": "^3.3.3", + "helmet": "^6.0.1", + "joi": "^17.8.4", + "moment": "^2.29.4", + "mongoose": "^7.0.2", + "morgan": "^1.10.0", + "nest-winston": "^1.9.1", + "nestjs-command": "^3.1.3", + "nestjs-i18n": "^10.2.6", + "passport": "^0.6.0", + "passport-headerapikey": "^1.2.2", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.1.13", + "response-time": "^2.3.2", + "rimraf": "^4.4.0", + "rotating-file-stream": "^3.1.0", + "rxjs": "^7.8.0", + "ua-parser-js": "^1.0.34", + "winston": "^3.8.2", + "winston-daily-rotate-file": "^4.7.1", + "xlsx": "^0.18.5", + "yargs": "^17.7.1", + "yarn": "^1.22.19" }, - "scripts": { - "test": "tsc; mocha --full-trace mocha \"spec/**/*.spec.js\"", - "test-with-coverage": "istanbul cover node_modules/.bin/_mocha -- 'spec/**/*.spec.js'", - "lint": "tslint --project=./tsconfig.json", - "build": "tsc -p .", - "dev": "tsc -p . --declaration -w", - "typings": "tsc --declaration", - "docs": "npx typedoc src/index.ts", - "dev-test-watch": "mocha-typescript-watch" - }, - "homepage": "https://git.osr-plastic.org/plastichub/lib-content", - "repository": { - "type": "git", - "url": "https://git.osr-plastic.org/plastichub/lib-content.git" - }, - "engines": { - "node": ">= 14.0.0" - }, - "license": "BSD-3-Clause", - "keywords": [ - "typescript" - ] + "devDependencies": { + "@nestjs/cli": "^9.2.0", + "@nestjs/schematics": "^9.0.4", + "@nestjs/testing": "^9.3.10", + "@types/bcryptjs": "^2.4.2", + "@types/bytes": "^3.1.1", + "@types/cors": "^2.8.13", + "@types/cron": "^2.0.0", + "@types/crypto-js": "^4.1.1", + "@types/express": "^4.17.17", + "@types/jest": "^29.4.4", + "@types/lodash": "^4.14.191", + "@types/morgan": "^1.9.4", + "@types/ms": "^0.7.31", + "@types/multer": "^1.4.7", + "@types/node": "^18.15.3", + "@types/passport-jwt": "^3.0.8", + "@types/supertest": "^2.0.12", + "@types/ua-parser-js": "^0.7.36", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.55.0", + "@typescript-eslint/parser": "^5.55.0", + "cspell": "^6.30.0", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-import": "^2.27.5", + "husky": "^8.0.3", + "jest": "^29.5.0", + "prettier": "^2.8.4", + "supertest": "^6.3.3", + "ts-jest": "^29.0.5", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "ts-prune": "^0.10.3", + "tsconfig-paths": "^4.1.2", + "typescript": "^5.0.2" + } } diff --git a/scripts/docker/dockerfile.prod b/scripts/docker/dockerfile.prod new file mode 100644 index 0000000..2865d15 --- /dev/null +++ b/scripts/docker/dockerfile.prod @@ -0,0 +1,31 @@ +# Test Image +FROM node:lts-alpine as builder +LABEL maintainer "fedi.dayeg@gmail.com" + +WORKDIR /app +COPY package.json yarn.lock ./ + +RUN set -x && yarn --production=false + +COPY . . + +RUN yarn build + +# Production Image +FROM node:lts-alpine as main +LABEL maintainer "fedi.dayeg@gmail.com" + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /app +EXPOSE 3000 + +COPY package.json yarn.lock ./ +RUN touch .env + +RUN set -x && yarn --production=true + +COPY --from=builder /app/dist ./dist + +CMD ["yarn", "start:prod"] \ No newline at end of file diff --git a/scripts/jenkinsfile b/scripts/jenkinsfile new file mode 100644 index 0000000..9f41c68 --- /dev/null +++ b/scripts/jenkinsfile @@ -0,0 +1,193 @@ +def commit_id + +pipeline { + agent any + options { + skipDefaultCheckout true + } + environment { + BASE_IMAGE='node:lts-alpine' // change this with same version of container image + + APP_NAME = 'nest' // this will be container name + APP_NETWORK = 'app-network' // this network name + APP_PORT = 3000 // this project will serve in port 0.0.0.0:3000 + APP_EXPOSE_PORT = 3000 // this project will expose at port 0.0.0.0:3000 + + NODE_JS = 'lts' // depends with our jenkins setting + + HOST_IP = 'xx.xx.xx.xx' // this server ip for your production server + HOST_CREDENTIAL = '2637b88f-8dc8-4395-bd6b-0c6127720a89' // depends with our credentials jenkins + + DOCKER_CREDENTIAL = 'ef108994-1241-4614-aab5-2aadd7a72284' // depends with our credentials jenkins + DOCKER_FILE= './prod/dockerfile' + DOCKER_USERNAME = 'nest' // docker hub username + DOCKER_REGISTRY = 'https://index.docker.io/v1/' + + GIT = 'Default' // depends with our jenkins setting + GIT_BRANCH = 'main' // git branch + GIT_CREDENTIAL = '86535ad6-5d74-48c0-9852-bddbe1fbaff6' // depends with our credentials jenkins + GIT_URL = 'git@github.com:fedi-dayeg/nestjs-mongoose.git' + } + tools { + nodejs NODE_JS + git GIT + } + stages { + stage('Prepare') { + steps { + cleanWs() + checkout scm + + sh 'node --version && npm --version && yarn --version' + sh 'docker --version' + sh 'docker ps -a' + + script{ + def nodeContainer = docker.image(BASE_IMAGE) + nodeContainer.pull() + nodeContainer.inside { + sh 'node --version' + sh 'npm --version' + sh 'yarn --version' + } + } + } + } + stage('Clone') { + steps { + git branch: GIT_BRANCH, + credentialsId: GIT_CREDENTIAL, + url: GIT_URL + + sh "git rev-parse --short HEAD > .git/commit-id" + sh "grep -o '\"version\": \"[^\"]*' package.json | grep -o '[^\"]*\$' > .git/version-id" + + script{ + commit_id = readFile('.git/commit-id').trim() + } + } + } + stage('Build'){ + steps{ + script{ + def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}" + docker.build(app_image, "--target builder -f ${DOCKER_FILE} .") + } + } + } + stage('Unit Test') { + steps { + script{ + def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}" + def container = "${APP_NAME}-testing" + + try{ + sh "docker stop ${container} && docker rm ${container}" + }catch(e){} + + try{ + sh "docker network create ${APP_NETWORK} --driver=bridge" + }catch(e){} + + sh "docker run --rm --network ${APP_NETWORK} \ + --volume /app/${APP_NAME}/.env:/app/.env \ + --name ${container} \ + ${app_image} \ + sh -c 'yarn test:unit'" + } + + } + } + stage('Push') { + steps { + script{ + def version_id = readFile('.git/version-id').trim() + def app_image = "${DOCKER_USERNAME}/${APP_NAME}:${commit_id}" + + def app = docker.build(app_image, "--target main -f ${DOCKER_FILE} .") + docker.withRegistry(DOCKER_REGISTRY, DOCKER_CREDENTIAL) { + app.push('latest') + app.push("v${version_id}") + app.push("v${version_id}_sha-${commit_id}") + } + } + + } + } + stage('Deploy') { + steps { + script{ + def version_id = readFile('.git/version-id').trim() + def app_image = "${DOCKER_USERNAME}/${APP_NAME}:v${version_id}_sha-${commit_id}" + + def remote = [:] + remote.name = APP_NAME + remote.host = HOST_IP + remote.allowAnyHosts = true + withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) { + + remote.user = USERNAME + remote.identityFile = IDENTITY + + try{ + sshCommand remote: remote, command: "docker stop ${APP_NAME} && docker rm ${APP_NAME}" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker network create ${APP_NETWORK} --driver=bridge" + }catch(e){} + + sshCommand remote: remote, command: "docker run -itd \ + --hostname ${APP_NAME} \ + --publish ${APP_EXPOSE_PORT}:${APP_PORT} \ + --network ${APP_NETWORK} \ + --volume /app/${APP_NAME}/logs/:/app/logs/ \ + --volume /app/${APP_NAME}/.env:/app/.env \ + --restart unless-stopped \ + --name ${APP_NAME} ${app_image}" + } + } + } + } + stage('Clean'){ + steps { + script{ + def remote = [:] + remote.name = APP_NAME + remote.host = HOST_IP + remote.allowAnyHosts = true + withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) { + + remote.user = USERNAME + remote.identityFile = IDENTITY + + try{ + sshCommand remote: remote, command: "docker container prune --force" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker image prune --force" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker rmi \$(docker images **/${APP_NAME}** -q) --force" + }catch(e){} + } + + try{ + sh "docker container prune --force" + }catch(e){} + + try{ + sh "docker image prune --force" + }catch(e){} + + try{ + sh "docker rmi \$(docker images **/${APP_NAME}** -q) --force" + }catch(e){} + + } + } + } + } +} \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..451a91a --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { JobsModule } from 'src/jobs/jobs.module'; +import { AppController } from './controllers/app.controller'; +import { RouterModule } from 'src/router/router.module'; +import { CommonModule } from 'src/common/common.module'; + +@Module({ + controllers: [AppController], + providers: [], + imports: [ + CommonModule, + + // Jobs + JobsModule.forRoot(), + + // Routes + RouterModule.forRoot(), + ], +}) +export class AppModule {} diff --git a/src/app/constants/app.constant.ts b/src/app/constants/app.constant.ts new file mode 100644 index 0000000..58273f5 --- /dev/null +++ b/src/app/constants/app.constant.ts @@ -0,0 +1 @@ +export const APP_LANGUAGE = 'en'; diff --git a/src/app/constants/app.enum.constant.ts b/src/app/constants/app.enum.constant.ts new file mode 100644 index 0000000..96033ea --- /dev/null +++ b/src/app/constants/app.enum.constant.ts @@ -0,0 +1,5 @@ +export enum ENUM_APP_ENVIRONMENT { + PRODUCTION = 'production', + STAGING = 'staging', + DEVELOPMENT = 'development', +} diff --git a/src/app/controllers/app.controller.ts b/src/app/controllers/app.controller.ts new file mode 100644 index 0000000..12a1224 --- /dev/null +++ b/src/app/controllers/app.controller.ts @@ -0,0 +1,81 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiTags } from '@nestjs/swagger'; +import { AppHelloApiKeyDoc, AppHelloDoc } from 'src/app/docs/app.doc'; +import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization'; +import { ApiKeyProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { Logger } from 'src/common/logger/decorators/logger.decorator'; +import { RequestUserAgent } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { IResult } from 'ua-parser-js'; + +@ApiTags('hello') +@Controller({ + version: VERSION_NEUTRAL, + path: '/', +}) +export class AppController { + private readonly serviceName: string; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.serviceName = this.configService.get('app.name'); + } + + @AppHelloDoc() + @Response('app.hello', { serialization: AppHelloSerialization }) + @Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] }) + @Get('/hello') + async hello(@RequestUserAgent() userAgent: IResult): Promise { + const newDate = this.helperDateService.create(); + + return { + _metadata: { + customProperty: { + messageProperties: { + serviceName: this.serviceName + }, + + }, + }, + data: { + userAgent, + date: newDate, + format: this.helperDateService.format(newDate), + timestamp: this.helperDateService.timestamp(newDate), + }, + }; + } + + @AppHelloApiKeyDoc() + @Response('app.hello', { serialization: AppHelloSerialization }) + @Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] }) + @ApiKeyProtected() + @Get('/hello/api-key') + async helloApiKey( + @RequestUserAgent() userAgent: IResult + ): Promise { + const newDate = this.helperDateService.create(); + + return { + _metadata: { + customProperty: { + messageProperties: { + serviceName: this.serviceName + } + }, + }, + data: { + userAgent, + date: newDate, + format: this.helperDateService.format(newDate), + timestamp: this.helperDateService.timestamp(newDate), + }, + }; + } +} diff --git a/src/app/docs/app.doc.ts b/src/app/docs/app.doc.ts new file mode 100644 index 0000000..54730f9 --- /dev/null +++ b/src/app/docs/app.doc.ts @@ -0,0 +1,30 @@ +import { applyDecorators } from '@nestjs/common'; +import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; + +export function AppHelloDoc(): MethodDecorator { + return applyDecorators( + Doc('app.hello', { + response: { + serialization: AppHelloSerialization, + }, + }) + ); +} + +export function AppHelloApiKeyDoc(): MethodDecorator { + return applyDecorators( + Doc('app.helloApiKey', { + auth: { + apiKey: true, + }, + requestHeader: { + timestamp: true, + userAgent: true, + }, + response: { + serialization: AppHelloSerialization, + }, + }) + ); +} diff --git a/src/app/serializations/app.hello.serialization.ts b/src/app/serializations/app.hello.serialization.ts new file mode 100644 index 0000000..6e5a75f --- /dev/null +++ b/src/app/serializations/app.hello.serialization.ts @@ -0,0 +1,30 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IResult } from 'ua-parser-js'; + +export class AppHelloSerialization { + @ApiProperty({ + example: { + ua: 'PostmanRuntime/7.29.0', + browser: {}, + engine: {}, + os: {}, + device: {}, + cpu: {}, + }, + }) + readonly userAgent: IResult; + + @ApiProperty({ example: faker.date.recent() }) + @Type(() => String) + readonly date: Date; + + @ApiProperty({ example: faker.date.recent() }) + readonly format: string; + + @ApiProperty({ + example: 1660190937231, + }) + readonly timestamp: number; +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..e6dcf6f --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,22 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { CommandModule, CommandService } from 'nestjs-command'; +import { MigrationModule } from './migration/migration.module'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(MigrationModule, { + logger: ['error'], + }); + + const logger = new Logger(); + + try { + await app.select(CommandModule).get(CommandService).exec(); + process.exit(0); + } catch (err: unknown) { + logger.error(err, 'Migration'); + process.exit(1); + } +} + +bootstrap(); diff --git a/src/common/api-key/api-key.module.ts b/src/common/api-key/api-key.module.ts new file mode 100644 index 0000000..2c22ced --- /dev/null +++ b/src/common/api-key/api-key.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyXApiKeyStrategy } from 'src/common/api-key/guards/x-api-key/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'; + +@Module({ + providers: [ApiKeyService, ApiKeyXApiKeyStrategy], + exports: [ApiKeyService], + controllers: [], + imports: [ApiKeyRepositoryModule], +}) +export class ApiKeyModule {} diff --git a/src/common/api-key/constants/api-key.constant.ts b/src/common/api-key/constants/api-key.constant.ts new file mode 100644 index 0000000..5efb20c --- /dev/null +++ b/src/common/api-key/constants/api-key.constant.ts @@ -0,0 +1 @@ +export const API_KEY_ACTIVE_META_KEY = 'ApiKeyActiveMetaKey'; diff --git a/src/common/api-key/constants/api-key.doc.ts b/src/common/api-key/constants/api-key.doc.ts new file mode 100644 index 0000000..947dc0a --- /dev/null +++ b/src/common/api-key/constants/api-key.doc.ts @@ -0,0 +1,22 @@ +import { faker } from '@faker-js/faker'; + +export const ApiKeyDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const ApiKeyDocParamsGet = [ + { + name: 'apiKey', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/common/api-key/constants/api-key.list.constant.ts b/src/common/api-key/constants/api-key.list.constant.ts new file mode 100644 index 0000000..3a71adf --- /dev/null +++ b/src/common/api-key/constants/api-key.list.constant.ts @@ -0,0 +1,9 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const API_KEY_DEFAULT_PER_PAGE = 20; +export const API_KEY_DEFAULT_ORDER_BY = 'createdAt'; +export const API_KEY_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const API_KEY_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'key', 'createdAt']; +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/constants/api-key.status-code.constant.ts b/src/common/api-key/constants/api-key.status-code.constant.ts new file mode 100644 index 0000000..577ea22 --- /dev/null +++ b/src/common/api-key/constants/api-key.status-code.constant.ts @@ -0,0 +1,10 @@ +export enum ENUM_API_KEY_STATUS_CODE_ERROR { + API_KEY_NEEDED_ERROR = 5120, + API_KEY_NOT_FOUND_ERROR = 5121, + API_KEY_IS_ACTIVE_ERROR = 5122, + API_KEY_EXPIRED_ERROR = 5123, + API_KEY_INACTIVE_ERROR = 5124, + API_KEY_INVALID_ERROR = 5125, + API_KEY_WRONG_ERROR = 5126, + API_KEY_EXIST_ERROR = 5127, +} diff --git a/src/common/api-key/controllers/api-key.admin.controller.ts b/src/common/api-key/controllers/api-key.admin.controller.ts new file mode 100644 index 0000000..96a0116 --- /dev/null +++ b/src/common/api-key/controllers/api-key.admin.controller.ts @@ -0,0 +1,118 @@ +import { + Body, + Controller, + InternalServerErrorException, + Patch, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + ApiKeyUpdateActiveGuard, + ApiKeyUpdateGuard, + ApiKeyUpdateInactiveGuard, +} from 'src/common/api-key/decorators/api-key.admin.decorator'; +import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator'; +import { + ApiKeyActiveDoc, + ApiKeyInactiveDoc, + ApiKeyUpdateDoc, +} from 'src/common/api-key/docs/api-key.admin.doc'; +import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +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_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +@ApiTags('admin.apiKey') +@Controller({ + version: '1', + path: '/api-key', +}) +export class ApiKeyAdminController { + constructor(private readonly apiKeyService: ApiKeyService) {} + + @ApiKeyInactiveDoc() + @Response('apiKey.inactive') + @ApiKeyUpdateInactiveGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_INACTIVE + ) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/inactive') + async inactive(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + await this.apiKeyService.inactive(apiKey); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @ApiKeyActiveDoc() + @Response('apiKey.active') + @ApiKeyUpdateActiveGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_ACTIVE + ) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/active') + async active(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + await this.apiKeyService.active(apiKey); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @ApiKeyUpdateDoc() + @Response('apiKey.updateDate', { serialization: ResponseIdSerialization }) + @ApiKeyUpdateGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE_DATE + ) + @AuthJwtAccessProtected() + @Put('/update/:apiKey/date') + async updateDate( + @Body() body: ApiKeyUpdateDateDto, + @GetApiKey() apiKey: ApiKeyDoc + ): Promise { + try { + await this.apiKeyService.updateDate(apiKey, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { data: { _id: apiKey._id } }; + } +} diff --git a/src/common/api-key/controllers/api-key.controller.ts b/src/common/api-key/controllers/api-key.controller.ts new file mode 100644 index 0000000..810c938 --- /dev/null +++ b/src/common/api-key/controllers/api-key.controller.ts @@ -0,0 +1,222 @@ +import { + Body, + ConflictException, + Controller, + Get, + InternalServerErrorException, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + API_KEY_DEFAULT_AVAILABLE_ORDER_BY, + API_KEY_DEFAULT_AVAILABLE_SEARCH, + API_KEY_DEFAULT_IS_ACTIVE, + API_KEY_DEFAULT_ORDER_BY, + API_KEY_DEFAULT_ORDER_DIRECTION, + API_KEY_DEFAULT_PER_PAGE, +} from 'src/common/api-key/constants/api-key.list.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { + ApiKeyGetGuard, + ApiKeyUpdateGuard, + ApiKeyUpdateResetGuard, +} from 'src/common/api-key/decorators/api-key.admin.decorator'; +import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator'; +import { + ApiKeyCreateDoc, + ApiKeyGetDoc, + ApiKeyListDoc, + ApiKeyResetDoc, + ApiKeyUpdateDoc, +} from 'src/common/api-key/docs/api-key.admin.doc'; +import { ApiKeyCreateDto } from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; +import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization'; +import { ApiKeyResetSerialization } from 'src/common/api-key/serializations/api-key.reset.serialization'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, +} 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 { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +@ApiTags('apiKey') +@Controller({ + version: '1', + path: '/api-key', +}) +export class ApiKeyController { + constructor( + private readonly apiKeyService: ApiKeyService, + private readonly paginationService: PaginationService + ) {} + + @ApiKeyListDoc() + @ResponsePaging('apiKey.list', { + serialization: ApiKeyListSerialization, + }) + @AuthJwtAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + API_KEY_DEFAULT_PER_PAGE, + API_KEY_DEFAULT_ORDER_BY, + API_KEY_DEFAULT_ORDER_DIRECTION, + API_KEY_DEFAULT_AVAILABLE_SEARCH, + API_KEY_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', API_KEY_DEFAULT_IS_ACTIVE) + isActive: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + }; + + const apiKeys: ApiKeyEntity[] = await this.apiKeyService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + const total: number = await this.apiKeyService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { totalPage, total }, + data: apiKeys, + }; + } + + @ApiKeyGetDoc() + @Response('apiKey.get', { + serialization: ApiKeyGetSerialization, + }) + @ApiKeyGetGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Get('get/:apiKey') + async get(@GetApiKey(true) apiKey: ApiKeyEntity): Promise { + return { data: apiKey }; + } + + @ApiKeyCreateDoc() + @Response('apiKey.create', { serialization: ApiKeyCreateSerialization }) + @AuthJwtAccessProtected() + @Post('/create') + async create( + @AuthJwtPayload('_id') _id: string, + @Body() body: ApiKeyCreateDto + ): Promise { + const checkUser: boolean = await this.apiKeyService.existByUser(_id); + if (checkUser) { + throw new ConflictException({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXIST_ERROR, + message: 'apiKey.error.exist', + }); + } + + try { + const created: IApiKeyCreated = await this.apiKeyService.create( + _id, + body + ); + + return { + data: { + _id: created.doc._id, + secret: created.secret, + }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @ApiKeyResetDoc() + @Response('apiKey.reset', { serialization: ApiKeyResetSerialization }) + @ApiKeyUpdateResetGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/reset') + async reset(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + const secret: string = await this.apiKeyService.createSecret(); + const updated: ApiKeyDoc = await this.apiKeyService.reset( + apiKey, + secret + ); + + return { + data: { + _id: updated._id, + secret, + }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @ApiKeyUpdateDoc() + @Response('apiKey.update', { serialization: ResponseIdSerialization }) + @ApiKeyUpdateGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Put('/update/:apiKey') + async updateName( + @Body() body: ApiKeyUpdateDto, + @GetApiKey() apiKey: ApiKeyDoc + ): Promise { + try { + await this.apiKeyService.update(apiKey, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { data: { _id: apiKey._id } }; + } +} \ No newline at end of file diff --git a/src/common/api-key/decorators/api-key.admin.decorator.ts b/src/common/api-key/decorators/api-key.admin.decorator.ts new file mode 100644 index 0000000..30ae7c8 --- /dev/null +++ b/src/common/api-key/decorators/api-key.admin.decorator.ts @@ -0,0 +1,60 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; +import { ApiKeyActiveGuard } from 'src/common/api-key/guards/api-key.active.guard'; +import { ApiKeyExpiredGuard } from 'src/common/api-key/guards/api-key.expired.guard'; +import { ApiKeyNotFoundGuard } from 'src/common/api-key/guards/api-key.not-found.guard'; +import { ApiKeyPutToRequestGuard } from 'src/common/api-key/guards/api-key.put-to-request.guard'; + +export function ApiKeyGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(ApiKeyPutToRequestGuard, ApiKeyNotFoundGuard) + ); +} + +export function ApiKeyUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} + +export function ApiKeyUpdateResetGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} + +export function ApiKeyUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [false]) + ); +} + +export function ApiKeyUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/common/api-key/decorators/api-key.decorator.ts b/src/common/api-key/decorators/api-key.decorator.ts new file mode 100644 index 0000000..31aa783 --- /dev/null +++ b/src/common/api-key/decorators/api-key.decorator.ts @@ -0,0 +1,27 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + UseGuards, +} from '@nestjs/common'; +import { ApiKeyXApiKeyGuard } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.guard'; +import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; + +export const ApiKeyPayload: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): IApiKeyPayload => { + const { apiKey } = ctx.switchToHttp().getRequest(); + return data ? apiKey[data] : apiKey; + } +); + +export function ApiKeyProtected(): MethodDecorator { + return applyDecorators(UseGuards(ApiKeyXApiKeyGuard)); +} + +export const GetApiKey = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): ApiKeyDoc => { + const { __apiKey } = ctx.switchToHttp().getRequest(); + return returnPlain ? __apiKey.toObject() : __apiKey; + } +); diff --git a/src/common/api-key/docs/api-key.admin.doc.ts b/src/common/api-key/docs/api-key.admin.doc.ts new file mode 100644 index 0000000..c2a2ccc --- /dev/null +++ b/src/common/api-key/docs/api-key.admin.doc.ts @@ -0,0 +1,119 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + ApiKeyDocParamsGet, + ApiKeyDocQueryIsActive, +} from 'src/common/api-key/constants/api-key.doc'; +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; +import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +export function ApiKeyListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('apiKey.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: ApiKeyDocQueryIsActive, + }, + response: { + serialization: ApiKeyListSerialization, + }, + }) + ); +} + +export function ApiKeyGetDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { serialization: ApiKeyGetSerialization }, + }) + ); +} + +export function ApiKeyCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ApiKeyCreateSerialization, + }, + }) + ); +} + +export function ApiKeyActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + }) + ); +} + +export function ApiKeyInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + }) + ); +} + +export function ApiKeyResetDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.reset', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { + serialization: ApiKeyCreateSerialization, + }, + }) + ); +} + +export function ApiKeyUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { + serialization: ResponseIdSerialization, + }, + }) + ); +} diff --git a/src/common/api-key/dtos/api-key.active.dto.ts b/src/common/api-key/dtos/api-key.active.dto.ts new file mode 100644 index 0000000..c21d079 --- /dev/null +++ b/src/common/api-key/dtos/api-key.active.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class ApiKeyActiveDto { + @ApiProperty({ + name: 'isActive', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/common/api-key/dtos/api-key.create.dto.ts b/src/common/api-key/dtos/api-key.create.dto.ts new file mode 100644 index 0000000..d8c403f --- /dev/null +++ b/src/common/api-key/dtos/api-key.create.dto.ts @@ -0,0 +1,57 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; + +export class ApiKeyCreateDto extends PartialType(ApiKeyUpdateDateDto) { + @ApiProperty({ + description: 'Api Key name', + example: `testapiname`, + required: true, + }) + @IsNotEmpty() + @IsString() + @MaxLength(50) + name: string; + + @ApiProperty({ + description: 'Description of api key', + example: 'blabla description', + required: false, + }) + @IsOptional() + @IsString() + @MinLength(1) + @MaxLength(100) + description?: string; +} + +export class ApiKeyCreateRawDto extends ApiKeyCreateDto { + @ApiProperty({ + name: 'key', + example: faker.random.alphaNumeric(10), + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsString() + @MaxLength(50) + key: string; + + @ApiProperty({ + name: 'secret', + example: faker.random.alphaNumeric(20), + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsString() + @MaxLength(100) + secret: string; +} diff --git a/src/common/api-key/dtos/api-key.request.dto.ts b/src/common/api-key/dtos/api-key.request.dto.ts new file mode 100644 index 0000000..a7f3c3f --- /dev/null +++ b/src/common/api-key/dtos/api-key.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class ApiKeyRequestDto { + @ApiProperty({ + name: 'apiKey', + description: 'apiKey id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + apiKey: string; +} diff --git a/src/common/api-key/dtos/api-key.update-date.dto.ts b/src/common/api-key/dtos/api-key.update-date.dto.ts new file mode 100644 index 0000000..10f36fb --- /dev/null +++ b/src/common/api-key/dtos/api-key.update-date.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { faker } from '@faker-js/faker'; +import { IsDate, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; +import { MinGreaterThanEqual } from 'src/common/request/validations/request.min-greater-than-equal.validation'; +import { MinDateToday } from 'src/common/request/validations/request.min-date-today.validation'; + +export class ApiKeyUpdateDateDto { + @ApiProperty({ + description: 'Api Key start date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + @MinDateToday() + startDate: Date; + + @ApiProperty({ + description: 'Api Key end date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + @MinGreaterThanEqual('startDate') + endDate: Date; +} diff --git a/src/common/api-key/dtos/api-key.update.dto.ts b/src/common/api-key/dtos/api-key.update.dto.ts new file mode 100644 index 0000000..5b18839 --- /dev/null +++ b/src/common/api-key/dtos/api-key.update.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ApiKeyCreateDto } from './api-key.create.dto'; + +export class ApiKeyUpdateDto extends PickType(ApiKeyCreateDto, [ + 'name', + 'description', +] as const) {} diff --git a/src/common/api-key/guards/api-key.active.guard.ts b/src/common/api-key/guards/api-key.active.guard.ts new file mode 100644 index 0000000..c19b0dd --- /dev/null +++ b/src/common/api-key/guards/api-key.active.guard.ts @@ -0,0 +1,36 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; + +@Injectable() +export class ApiKeyActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + API_KEY_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __apiKey } = context.switchToHttp().getRequest(); + + if (!required.includes(__apiKey.isActive)) { + throw new BadRequestException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR, + message: 'apiKey.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.expired.guard.ts b/src/common/api-key/guards/api-key.expired.guard.ts new file mode 100644 index 0000000..b512209 --- /dev/null +++ b/src/common/api-key/guards/api-key.expired.guard.ts @@ -0,0 +1,31 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ApiKeyExpiredGuard implements CanActivate { + constructor(private readonly helperDateService: HelperDateService) {} + + async canActivate(context: ExecutionContext): Promise { + const { __apiKey } = context.switchToHttp().getRequest(); + const today: Date = this.helperDateService.create(); + + if ( + __apiKey.startDate && + __apiKey.endDate && + today > __apiKey.endDate + ) { + throw new BadRequestException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR, + message: 'apiKey.error.expired', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.not-found.guard.ts b/src/common/api-key/guards/api-key.not-found.guard.ts new file mode 100644 index 0000000..c11bcf7 --- /dev/null +++ b/src/common/api-key/guards/api-key.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; + +@Injectable() +export class ApiKeyNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __apiKey } = context.switchToHttp().getRequest(); + + if (!__apiKey) { + throw new NotFoundException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + message: 'apiKey.error.notFound', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.put-to-request.guard.ts b/src/common/api-key/guards/api-key.put-to-request.guard.ts new file mode 100644 index 0000000..9db837c --- /dev/null +++ b/src/common/api-key/guards/api-key.put-to-request.guard.ts @@ -0,0 +1,19 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; + +@Injectable() +export class ApiKeyPutToRequestGuard implements CanActivate { + constructor(private readonly apiKeyService: ApiKeyService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { apiKey } = params; + + const check: ApiKeyDoc = await this.apiKeyService.findOneById(apiKey); + request.__apiKey = check; + + return true; + } +} diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts b/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts new file mode 100644 index 0000000..86dfcd7 --- /dev/null +++ b/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts @@ -0,0 +1,86 @@ +import { AuthGuard } from '@nestjs/passport'; +import { + ExecutionContext, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { BadRequestError } from 'passport-headerapikey'; + +@Injectable() +export class ApiKeyXApiKeyGuard extends AuthGuard('api-key') { + constructor(private readonly helperNumberService: HelperNumberService) { + super(); + } + + canActivate(context: ExecutionContext) { + return super.canActivate(context); + } + + handleRequest( + err: Record, + apiKey: IApiKeyPayload, + info: Error | string + ): IApiKeyPayload { + if (err || !apiKey) { + if ( + info instanceof BadRequestError && + info.message === 'Missing API Key' + ) { + throw new UnauthorizedException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + message: 'apiKey.error.keyNeeded', + }); + } else if (err) { + const statusCode: number = this.helperNumberService.create( + err.message as string + ); + + if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.notFound', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.inactive', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.expired', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_WRONG_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.wrong', + }); + } + } + + throw new UnauthorizedException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + message: 'apiKey.error.invalid', + }); + } + + return apiKey; + } +} diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts b/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts new file mode 100644 index 0000000..dc56be5 --- /dev/null +++ b/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import Strategy from 'passport-headerapikey'; +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 { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class ApiKeyXApiKeyStrategy extends PassportStrategy( + Strategy, + 'api-key' +) { + constructor( + private readonly apiKeyService: ApiKeyService, + private readonly helperDateService: HelperDateService + ) { + super( + { header: 'X-API-KEY', prefix: '' }, + true, + async ( + apiKey: string, + verified: ( + error: Error, + user?: Record, + info?: string | number + ) => Promise, + req: IRequestApp + ) => this.validate(apiKey, verified, req) + ); + } + + async validate( + apiKey: string, + verified: ( + error: Error, + user?: ApiKeyEntity, + info?: string | number + ) => Promise, + req: IRequestApp + ): Promise { + const xApiKey: string[] = apiKey.split(':'); + if (xApiKey.length !== 2) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}` + ), + null, + null + ); + + return; + } + + const key = xApiKey[0]; + const secret = xApiKey[1]; + const today = this.helperDateService.create(); + const authApi: ApiKeyEntity = + await this.apiKeyService.findOneByActiveKey(key); + + if (!authApi) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR}` + ), + null, + null + ); + + return; + } else if (!authApi.isActive) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR}` + ), + null, + null + ); + + return; + } else if ( + authApi.startDate && + authApi.endDate && + (authApi.startDate < today || authApi.endDate > today) + ) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR}` + ), + null, + null + ); + } + + const validateApiKey: boolean = + await this.apiKeyService.validateHashApiKey(secret, authApi.hash); + if (!validateApiKey) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}` + ), + null, + null + ); + + return; + } + + req.apiKey = { + _id: `${authApi._id}`, + key: authApi.key, + name: authApi.name, + }; + verified(null, authApi); + + return; + } +} diff --git a/src/common/api-key/interfaces/api-key.interface.ts b/src/common/api-key/interfaces/api-key.interface.ts new file mode 100644 index 0000000..c35117b --- /dev/null +++ b/src/common/api-key/interfaces/api-key.interface.ts @@ -0,0 +1,12 @@ +import { ApiKeyDoc } from "src/common/api-key/repository/entities/api-key.entity"; + +export interface IApiKeyPayload { + _id: string; + key: string; + name: string; +} + +export interface IApiKeyCreated { + secret: string; + doc: ApiKeyDoc; +} diff --git a/src/common/api-key/interfaces/api-key.service.interface.ts b/src/common/api-key/interfaces/api-key.service.interface.ts new file mode 100644 index 0000000..dea228d --- /dev/null +++ b/src/common/api-key/interfaces/api-key.service.interface.ts @@ -0,0 +1,92 @@ +import { + ApiKeyCreateDto, + ApiKeyCreateRawDto, +} from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseManyOptions, + IDatabaseOptions, +} from 'src/common/database/interfaces/database.interface'; + +export interface IApiKeyService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByActiveKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + user: string, + {name, description, startDate, endDate}: ApiKeyCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + createRaw( + user: string, + {name, description, key, secret, startDate, endDate}: ApiKeyCreateRawDto, + options?: IDatabaseCreateOptions + ): Promise; + + active(repository: ApiKeyDoc): Promise; + + inactive(repository: ApiKeyDoc): Promise; + + update(repository: ApiKeyDoc, data: ApiKeyUpdateDto): Promise; + + updateDate( + repository: ApiKeyDoc, + {startDate, endDate}: ApiKeyUpdateDateDto + ): Promise; + + reset(repository: ApiKeyDoc, secret: string): Promise; + + delete(repository: ApiKeyDoc): Promise; + + validateHashApiKey(hashFromRequest: string, hash: string): Promise; + + createKey(): Promise; + + createSecret(): Promise; + + createHashApiKey(key: string, secret: string): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; + + inactiveManyByEndDate(options?: IDatabaseManyOptions): Promise; +} diff --git a/src/common/api-key/repository/api-key.repository.module.ts b/src/common/api-key/repository/api-key.repository.module.ts new file mode 100644 index 0000000..24d7e8d --- /dev/null +++ b/src/common/api-key/repository/api-key.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +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'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; + +@Module({ + providers: [ApiKeyRepository], + exports: [ApiKeyRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: ApiKeyEntity.name, + schema: ApiKeySchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class ApiKeyRepositoryModule {} diff --git a/src/common/api-key/repository/entities/api-key.entity.ts b/src/common/api-key/repository/entities/api-key.entity.ts new file mode 100644 index 0000000..601908f --- /dev/null +++ b/src/common/api-key/repository/entities/api-key.entity.ts @@ -0,0 +1,85 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError } from 'mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { Document } from 'mongoose'; +import { UserEntity } from "src/modules/user/repository/entities/user.entity"; + +export const ApiKeyDatabaseName = 'apikeys'; + +@DatabaseEntity({ collection: ApiKeyDatabaseName }) +export class ApiKeyEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + ref: UserEntity.name, + index: true, + }) + user: string; + + @Prop({ + required: true, + index: true, + type: String, + minlength: 1, + maxlength: 100, + lowercase: true, + trim: true, + }) + name: string; + + @Prop({ + required: false, + type: String, + minlength: 1, + maxlength: 255, + }) + description?: string; + + @Prop({ + required: true, + type: String, + unique: true, + index: true, + trim: true, + }) + key: string; + + @Prop({ + required: true, + trim: true, + type: String, + }) + hash: string; + + @Prop({ + required: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: false, + type: Date, + }) + startDate?: Date; + + @Prop({ + required: false, + type: Date, + }) + endDate?: Date; +} + +export const ApiKeySchema = SchemaFactory.createForClass(ApiKeyEntity); + +export type ApiKeyDoc = ApiKeyEntity & Document; + +ApiKeySchema.pre( + 'save', + function (next: CallbackWithoutResultAndOptionalError) { + this.name = this.name.toLowerCase(); + + next(); + } +); diff --git a/src/common/api-key/repository/repositories/api-key.repository.ts b/src/common/api-key/repository/repositories/api-key.repository.ts new file mode 100644 index 0000000..b67973b --- /dev/null +++ b/src/common/api-key/repository/repositories/api-key.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { + ApiKeyEntity, + ApiKeyDoc, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; + +@Injectable() +export class ApiKeyRepository extends DatabaseMongoUUIDRepositoryAbstract< + ApiKeyEntity, + ApiKeyDoc +> { + constructor( + @DatabaseModel(ApiKeyEntity.name) + private readonly ApiKeyDoc: Model + ) { + super(ApiKeyDoc); + } +} diff --git a/src/common/api-key/serializations/api-key.create.serialization.ts b/src/common/api-key/serializations/api-key.create.serialization.ts new file mode 100644 index 0000000..185f848 --- /dev/null +++ b/src/common/api-key/serializations/api-key.create.serialization.ts @@ -0,0 +1,14 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; + +export class ApiKeyCreateSerialization extends PickType( + ApiKeyGetSerialization, + ['key', '_id'] as const +) { + @ApiProperty({ + description: 'Secret key of ApiKey, only show at once', + example: true, + required: true, + }) + secret: string; +} diff --git a/src/common/api-key/serializations/api-key.get.serialization.ts b/src/common/api-key/serializations/api-key.get.serialization.ts new file mode 100644 index 0000000..53d47b3 --- /dev/null +++ b/src/common/api-key/serializations/api-key.get.serialization.ts @@ -0,0 +1,70 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +export class ApiKeyGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Alias name of api key', + example: faker.name.jobTitle(), + required: true, + }) + name: string; + + @ApiProperty({ + description: 'Description of api key', + example: 'blabla description', + required: false, + }) + description?: string; + + @ApiProperty({ + description: 'Unique key of api key', + example: faker.random.alpha(115), + required: true, + }) + key: string; + + @Exclude() + hash: string; + + @ApiProperty({ + description: 'Active flag of api key', + example: true, + required: true, + }) + isActive: boolean; + + @ApiProperty({ + description: 'Api Key start date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + startDate?: Date; + + @ApiProperty({ + description: 'Api Key end date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + endDate?: Date; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: false, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/common/api-key/serializations/api-key.list.serialization.ts b/src/common/api-key/serializations/api-key.list.serialization.ts new file mode 100644 index 0000000..6c940d6 --- /dev/null +++ b/src/common/api-key/serializations/api-key.list.serialization.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; + +export class ApiKeyListSerialization extends OmitType(ApiKeyGetSerialization, [ + 'description', +] as const) {} diff --git a/src/common/api-key/serializations/api-key.reset.serialization.ts b/src/common/api-key/serializations/api-key.reset.serialization.ts new file mode 100644 index 0000000..2d6b9cd --- /dev/null +++ b/src/common/api-key/serializations/api-key.reset.serialization.ts @@ -0,0 +1,3 @@ +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; + +export class ApiKeyResetSerialization extends ApiKeyCreateSerialization {} diff --git a/src/common/api-key/services/api-key.service.ts b/src/common/api-key/services/api-key.service.ts new file mode 100644 index 0000000..455c955 --- /dev/null +++ b/src/common/api-key/services/api-key.service.ts @@ -0,0 +1,260 @@ +import { Injectable } from '@nestjs/common'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseExistOptions, +} from 'src/common/database/interfaces/database.interface'; +import { IApiKeyService } from 'src/common/api-key/interfaces/api-key.service.interface'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.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'; +import { ApiKeyActiveDto } from 'src/common/api-key/dtos/api-key.active.dto'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { ConfigService } from '@nestjs/config'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { + ApiKeyCreateDto, + ApiKeyCreateRawDto, +} from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ApiKeyService implements IApiKeyService { + private readonly env: string; + + constructor( + private readonly helperStringService: HelperStringService, + private readonly configService: ConfigService, + private readonly helperHashService: HelperHashService, + private readonly helperDateService: HelperDateService, + private readonly apiKeyRepository: ApiKeyRepository + ) { + this.env = this.configService.get('app.env'); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.apiKeyRepository.findAll(find, options); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne(find, options); + } + + async existByUser( + user: string, + options?: IDatabaseExistOptions + ): Promise { + return this.apiKeyRepository.exists( + { + user, + }, + options + ); + } + + async findOneByKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne({ key }, options); + } + + async findOneByActiveKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne( + { + key, + isActive: true, + }, + options + ); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.apiKeyRepository.getTotal(find, options); + } + + async create( + user: string, + { name, description, startDate, endDate }: ApiKeyCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const key = await this.createKey(); + const secret = await this.createSecret(); + const hash: string = await this.createHashApiKey(key, secret); + + const dto: ApiKeyEntity = new ApiKeyEntity(); + dto.user = user; + dto.name = name; + dto.description = description; + dto.key = key; + dto.hash = hash; + dto.isActive = true; + + if (startDate && endDate) { + dto.startDate = startDate; + dto.endDate = endDate; + } + + const created: ApiKeyDoc = + await this.apiKeyRepository.create(dto, options); + + return { doc: created, secret }; + } + + async createRaw( + user: string, + { + name, + description, + key, + secret, + startDate, + endDate, + }: ApiKeyCreateRawDto, + options?: IDatabaseCreateOptions + ): Promise { + const hash: string = await this.createHashApiKey(key, secret); + + const dto: ApiKeyEntity = new ApiKeyEntity(); + dto.name = name; + dto.user = user; + dto.description = description; + dto.key = key; + dto.hash = hash; + dto.isActive = true; + + if (startDate && endDate) { + dto.startDate = this.helperDateService.startOfDay(startDate); + dto.endDate = this.helperDateService.endOfDay(endDate); + } + + const created: ApiKeyDoc = + await this.apiKeyRepository.create(dto, options); + + return { doc: created, secret }; + } + + async active(repository: ApiKeyDoc): Promise { + repository.isActive = true; + + return this.apiKeyRepository.save(repository); + } + + async inactive(repository: ApiKeyDoc): Promise { + repository.isActive = false; + + return this.apiKeyRepository.save(repository); + } + + async update( + repository: ApiKeyDoc, + { name, description }: ApiKeyUpdateDto + ): Promise { + repository.name = name; + repository.description = description; + + return this.apiKeyRepository.save(repository); + } + + async updateDate( + repository: ApiKeyDoc, + { startDate, endDate }: ApiKeyUpdateDateDto + ): Promise { + repository.startDate = this.helperDateService.startOfDay(startDate); + repository.endDate = this.helperDateService.endOfDay(endDate); + + return this.apiKeyRepository.save(repository); + } + + async reset(repository: ApiKeyDoc, secret: string): Promise { + const hash: string = await this.createHashApiKey( + repository.key, + secret + ); + + repository.hash = hash; + + return this.apiKeyRepository.save(repository); + } + + async delete(repository: ApiKeyDoc): Promise { + return this.apiKeyRepository.softDelete(repository); + } + + async validateHashApiKey( + hashFromRequest: string, + hash: string + ): Promise { + return this.helperHashService.sha256Compare(hashFromRequest, hash); + } + + async createKey(): Promise { + return this.helperStringService.random(25, { + safe: false, + upperCase: true, + prefix: `${this.env}_`, + }); + } + + async createSecret(): Promise { + return this.helperStringService.random(35, { + safe: false, + upperCase: true, + }); + } + + async createHashApiKey(key: string, secret: string): Promise { + return this.helperHashService.sha256(`${key}:${secret}`); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.apiKeyRepository.deleteMany(find, options); + } + + async inactiveManyByEndDate( + options?: IDatabaseManyOptions + ): Promise { + return this.apiKeyRepository.updateMany( + { + endDate: { + $lte: this.helperDateService.create(), + }, + isActive: true, + }, + { + isActive: false, + }, + options + ); + } +} diff --git a/src/common/api-key/tasks/api-key.inactive.task.ts b/src/common/api-key/tasks/api-key.inactive.task.ts new file mode 100644 index 0000000..2a6a1c0 --- /dev/null +++ b/src/common/api-key/tasks/api-key.inactive.task.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; + +@Injectable() +export class ApiKeyInactiveTask { + constructor(private readonly apiKeyService: ApiKeyService) {} + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, { + name: 'inactiveApiKey', + }) + async inactiveApiKey(): Promise { + try { + await this.apiKeyService.inactiveManyByEndDate(); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/common/auth/auth.module.ts b/src/common/auth/auth.module.ts new file mode 100644 index 0000000..645ea13 --- /dev/null +++ b/src/common/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthJwtAccessStrategy } from 'src/common/auth/guards/jwt-access/auth.jwt-access.strategy'; +import { AuthJwtRefreshStrategy } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Module({ + providers: [AuthService, AuthJwtAccessStrategy, AuthJwtRefreshStrategy], + exports: [AuthService], + controllers: [], + imports: [], +}) +export class AuthModule {} diff --git a/src/common/auth/constants/auth.constant.ts b/src/common/auth/constants/auth.constant.ts new file mode 100644 index 0000000..87563cc --- /dev/null +++ b/src/common/auth/constants/auth.constant.ts @@ -0,0 +1,2 @@ +export const AUTH_ACCESS_FOR_META_KEY = 'AuthAccessForMetaKey'; +export const AUTH_PERMISSION_META_KEY = 'AuthPermissionMetaKey'; diff --git a/src/common/auth/constants/auth.enum.constant.ts b/src/common/auth/constants/auth.enum.constant.ts new file mode 100644 index 0000000..8cb673f --- /dev/null +++ b/src/common/auth/constants/auth.enum.constant.ts @@ -0,0 +1,17 @@ +export enum ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN { + SUPER_ADMIN = 'SUPER_ADMIN', +} + +export enum ENUM_AUTH_ACCESS_FOR_DEFAULT { + USER = 'USER', + ADMIN = 'ADMIN', +} + +export const ENUM_AUTH_ACCESS_FOR = { + ...ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN, + ...ENUM_AUTH_ACCESS_FOR_DEFAULT, +}; + +export type ENUM_AUTH_ACCESS_FOR = + | ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN + | ENUM_AUTH_ACCESS_FOR_DEFAULT; diff --git a/src/common/auth/constants/auth.enum.permission.constant.ts b/src/common/auth/constants/auth.enum.permission.constant.ts new file mode 100644 index 0000000..e00f179 --- /dev/null +++ b/src/common/auth/constants/auth.enum.permission.constant.ts @@ -0,0 +1,32 @@ +export enum ENUM_AUTH_PERMISSIONS { + API_KEY_READ = 'API_KEY_READ', + API_KEY_UPDATE = 'API_KEY_UPDATE', + API_KEY_ACTIVE = 'API_KEY_ACTIVE', + API_KEY_INACTIVE = 'API_KEY_INACTIVE', + API_KEY_UPDATE_DATE = 'API_KEY_UPDATE_DATE', + + USER_READ = 'USER_READ', + USER_CREATE = 'USER_CREATE', + USER_UPDATE = 'USER_UPDATE', + USER_ACTIVE = 'USER_ACTIVE', + USER_INACTIVE = 'USER_INACTIVE', + USER_BLOCKED = 'USER_BLOCKED', + USER_DELETE = 'USER_DELETE', + USER_IMPORT = 'USER_IMPORT', + USER_EXPORT = 'USER_EXPORT', + + ROLE_CREATE = 'ROLE_CREATE', + ROLE_UPDATE = 'ROLE_UPDATE', + ROLE_ACTIVE = 'ROLE_ACTIVE', + ROLE_INACTIVE = 'ROLE_INACTIVE', + ROLE_READ = 'ROLE_READ', + ROLE_DELETE = 'ROLE_DELETE', + + PERMISSION_READ = 'PERMISSION_READ', + PERMISSION_UPDATE = 'PERMISSION_UPDATE', + PERMISSION_ACTIVE = 'PERMISSION_ACTIVE', + PERMISSION_INACTIVE = 'PERMISSION_INACTIVE', + + SETTING_READ = 'SETTING_READ', + SETTING_UPDATE = 'SETTING_UPDATE', +} diff --git a/src/common/auth/constants/auth.status-code.constant.ts b/src/common/auth/constants/auth.status-code.constant.ts new file mode 100644 index 0000000..203b6b7 --- /dev/null +++ b/src/common/auth/constants/auth.status-code.constant.ts @@ -0,0 +1,10 @@ +export enum ENUM_AUTH_STATUS_CODE_ERROR { + AUTH_JWT_ACCESS_TOKEN_ERROR = 5100, + AUTH_JWT_REFRESH_TOKEN_ERROR = 5101, + AUTH_PERMISSION_TOKEN_ERROR = 5102, + AUTH_PERMISSION_TOKEN_INVALID_ERROR = 5103, + AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR = 5104, + AUTH_ROLE_ACCESS_FOR_INVALID_ERROR = 5105, + AUTH_PERMISSION_INVALID_ERROR = 5106, + AUTH_ACCESS_FOR_INVALID_ERROR = 5107, +} diff --git a/src/common/auth/decorators/auth.jwt.decorator.ts b/src/common/auth/decorators/auth.jwt.decorator.ts new file mode 100644 index 0000000..42534ef --- /dev/null +++ b/src/common/auth/decorators/auth.jwt.decorator.ts @@ -0,0 +1,49 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { AuthJwtAccessGuard } from 'src/common/auth/guards/jwt-access/auth.jwt-access.guard'; +import { AuthJwtRefreshGuard } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard'; +import { AuthPayloadAccessForGuard } from 'src/common/auth/guards/payload/auth.payload.access-for.guard'; +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const AuthJwtPayload = createParamDecorator( + (data: string, ctx: ExecutionContext): Record => { + const { user } = ctx.switchToHttp().getRequest(); + return data ? user[data] : user; + } +); + +export const AuthJwtToken = createParamDecorator( + (data: string, ctx: ExecutionContext): string => { + const { headers } = ctx.switchToHttp().getRequest(); + const { authorization } = headers; + const authorizations: string[] = authorization.split(' '); + + return authorizations.length >= 2 ? authorizations[1] : undefined; + } +); + +export function AuthJwtAccessProtected(): MethodDecorator { + return applyDecorators(UseGuards(AuthJwtAccessGuard)); +} + +export function AuthJwtPublicAccessProtected(): MethodDecorator { + return applyDecorators( + UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard), + SetMetadata(AUTH_ACCESS_FOR_META_KEY, [ENUM_AUTH_ACCESS_FOR.USER]) + ); +} + +export function AuthJwtAdminAccessProtected(): MethodDecorator { + return applyDecorators( + UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard), + SetMetadata(AUTH_ACCESS_FOR_META_KEY, [ + ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + ENUM_AUTH_ACCESS_FOR.ADMIN, + ]) + ); +} + +export function AuthJwtRefreshProtected(): MethodDecorator { + return applyDecorators(UseGuards(AuthJwtRefreshGuard)); +} diff --git a/src/common/auth/decorators/auth.permission.decorator.ts b/src/common/auth/decorators/auth.permission.decorator.ts new file mode 100644 index 0000000..63f6917 --- /dev/null +++ b/src/common/auth/decorators/auth.permission.decorator.ts @@ -0,0 +1,27 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseGuards, +} from '@nestjs/common'; +import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthPayloadPermissionGuard } from 'src/common/auth/guards/payload/auth.payload.permission.guard'; +import { AuthPermissionGuard } from 'src/common/auth/guards/permission/auth.permission.guard'; + +export const AuthPermissionPayload = createParamDecorator( + (data: string, ctx: ExecutionContext): Record => { + const { permissions } = ctx.switchToHttp().getRequest(); + return permissions; + } +); + +export function AuthPermissionProtected( + ...permissions: ENUM_AUTH_PERMISSIONS[] +): MethodDecorator { + return applyDecorators( + UseGuards(AuthPermissionGuard, AuthPayloadPermissionGuard), + SetMetadata(AUTH_PERMISSION_META_KEY, permissions) + ); +} diff --git a/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts b/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts new file mode 100644 index 0000000..96ee540 --- /dev/null +++ b/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts @@ -0,0 +1,19 @@ +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'; + +@Injectable() +export class AuthJwtAccessGuard extends AuthGuard('jwt') { + handleRequest(err: Error, user: TUser, info: Error): TUser { + if (err || !user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + _error: err ? err.message : info.message, + }); + } + + return user; + } +} diff --git a/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts b/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts new file mode 100644 index 0000000..5d25968 --- /dev/null +++ b/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts @@ -0,0 +1,40 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthJwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme( + configService.get('auth.prefixAuthorization') + ), + ignoreExpiration: false, + jsonWebTokenOptions: { + ignoreNotBefore: false, + audience: configService.get('auth.audience'), + issuer: configService.get('auth.issuer'), + subject: configService.get('auth.subject'), + }, + secretOrKey: configService.get( + 'auth.accessToken.secretKey' + ), + }); + } + + async validate({ + data, + }: Record): Promise> { + const payloadEncryption: boolean = + await this.authService.getPayloadEncryption(); + + return payloadEncryption + ? this.authService.decryptAccessToken({ data }) + : data; + } +} diff --git a/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts new file mode 100644 index 0000000..4082b0c --- /dev/null +++ b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts @@ -0,0 +1,19 @@ +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'; + +@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.AUTH_JWT_REFRESH_TOKEN_ERROR, + message: 'auth.error.refreshTokenUnauthorized', + _error: err ? err.message : info.message, + }); + } + + return user; + } +} diff --git a/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts new file mode 100644 index 0000000..969b790 --- /dev/null +++ b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts @@ -0,0 +1,43 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthJwtRefreshStrategy extends PassportStrategy( + Strategy, + 'jwtRefresh' +) { + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme( + configService.get('auth.prefixAuthorization') + ), + ignoreExpiration: false, + jsonWebTokenOptions: { + ignoreNotBefore: false, + audience: configService.get('auth.audience'), + issuer: configService.get('auth.issuer'), + subject: configService.get('auth.subject'), + }, + secretOrKey: configService.get( + 'auth.refreshToken.secretKey' + ), + }); + } + + async validate({ + data, + }: Record): Promise> { + const payloadEncryption: boolean = + await this.authService.getPayloadEncryption(); + + return payloadEncryption + ? this.authService.decryptRefreshToken({ data }) + : data; + } +} diff --git a/src/common/auth/guards/payload/auth.payload.access-for.guard.ts b/src/common/auth/guards/payload/auth.payload.access-for.guard.ts new file mode 100644 index 0000000..2842d58 --- /dev/null +++ b/src/common/auth/guards/payload/auth.payload.access-for.guard.ts @@ -0,0 +1,48 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; + +@Injectable() +export class AuthPayloadAccessForGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly helperArrayService: HelperArrayService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const requiredFor: ENUM_AUTH_ACCESS_FOR[] = + this.reflector.getAllAndOverride( + AUTH_ACCESS_FOR_META_KEY, + [context.getHandler(), context.getClass()] + ); + + const { user } = context.switchToHttp().getRequest(); + const { accessFor } = user; + + if (!requiredFor || accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) { + return true; + } + + const hasFor: boolean = this.helperArrayService.includes( + requiredFor, + accessFor + ); + + if (!hasFor) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + message: 'auth.error.accessForForbidden', + }); + } + return hasFor; + } +} diff --git a/src/common/auth/guards/payload/auth.payload.permission.guard.ts b/src/common/auth/guards/payload/auth.payload.permission.guard.ts new file mode 100644 index 0000000..705a1da --- /dev/null +++ b/src/common/auth/guards/payload/auth.payload.permission.guard.ts @@ -0,0 +1,57 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; + +@Injectable() +export class AuthPayloadPermissionGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly helperArrayService: HelperArrayService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const requiredPermission: ENUM_AUTH_PERMISSIONS[] = + this.reflector.getAllAndOverride( + AUTH_PERMISSION_META_KEY, + [context.getHandler(), context.getClass()] + ); + + const { permissions, user } = context.switchToHttp().getRequest(); + if (!user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + }); + } else if ( + !requiredPermission || + user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN + ) { + return true; + } + + const hasPermission: boolean = this.helperArrayService.in( + permissions, + requiredPermission + ); + + if (!hasPermission) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + message: 'auth.error.permissionForbidden', + }); + } + return hasPermission; + } +} diff --git a/src/common/auth/guards/permission/auth.permission.guard.ts b/src/common/auth/guards/permission/auth.permission.guard.ts new file mode 100644 index 0000000..1ed14d2 --- /dev/null +++ b/src/common/auth/guards/permission/auth.permission.guard.ts @@ -0,0 +1,81 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + ForbiddenException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthPermissionGuard implements CanActivate { + private readonly headerName: string; + + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + this.headerName = this.configService.get( + 'auth.permissionToken.headerName' + ); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { user } = request; + + if (!user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + }); + } else if (user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) { + return true; + } + + const { [this.headerName]: token } = request.headers; + + if (!token) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + message: 'auth.error.permissionTokenUnauthorized', + }); + } + + const validate: boolean = + await this.authService.validatePermissionToken(token); + if (!validate) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + message: 'auth.error.permissionTokenInvalid', + }); + } + + const { data } = await this.authService.payloadPermissionToken(token); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadDecryptPermissionToken: Record = data; + + if (payloadEncryption) { + payloadDecryptPermissionToken = + await this.authService.decryptPermissionToken(data); + } + + if (payloadDecryptPermissionToken._id !== user._id) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + message: 'auth.error.permissionTokenNotYour', + }); + } + request.permissions = payloadDecryptPermissionToken.permissions; + + return true; + } +} diff --git a/src/common/auth/interfaces/auth.interface.ts b/src/common/auth/interfaces/auth.interface.ts new file mode 100644 index 0000000..13865e9 --- /dev/null +++ b/src/common/auth/interfaces/auth.interface.ts @@ -0,0 +1,17 @@ +// Auth +export interface IAuthPassword { + salt: string; + passwordHash: string; + passwordExpired: Date; + passwordCreated: Date; +} + +export interface IAuthPayloadOptions { + loginDate: Date; +} + +export interface IAuthRefreshTokenOptions { + // in milis + notBeforeExpirationTime?: number | string; + rememberMe?: boolean; +} diff --git a/src/common/auth/interfaces/auth.service.interface.ts b/src/common/auth/interfaces/auth.service.interface.ts new file mode 100644 index 0000000..dd91466 --- /dev/null +++ b/src/common/auth/interfaces/auth.service.interface.ts @@ -0,0 +1,93 @@ +import { + IAuthPassword, + IAuthPayloadOptions, + IAuthRefreshTokenOptions, +} from 'src/common/auth/interfaces/auth.interface'; + +export interface IAuthService { + encryptAccessToken(payload: Record): Promise; + + decryptAccessToken( + payload: Record + ): Promise>; + + createAccessToken( + payloadHashed: string | Record + ): Promise; + + validateAccessToken(token: string): Promise; + + payloadAccessToken(token: string): Promise>; + + encryptRefreshToken(payload: Record): Promise; + + decryptRefreshToken( + payload: Record + ): Promise>; + + createRefreshToken( + payloadHashed: string | Record, + options?: IAuthRefreshTokenOptions + ): Promise; + + validateRefreshToken(token: string): Promise; + + payloadRefreshToken(token: string): Promise>; + + encryptPermissionToken(payload: Record): Promise; + + decryptPermissionToken({ + data, + }: Record): Promise>; + + createPermissionToken( + payloadHashed: string | Record + ): Promise; + + validatePermissionToken(token: string): Promise; + + payloadPermissionToken(token: string): Promise>; + + validateUser( + passwordString: string, + passwordHash: string + ): Promise; + + createPayloadAccessToken( + data: Record, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise>; + + createPayloadRefreshToken( + _id: string, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise>; + + createPayloadPermissionToken( + data: Record + ): Promise>; + + createSalt(length: number): Promise; + + createPassword(password: string): Promise; + + checkPasswordExpired(passwordExpired: Date): Promise; + + getTokenType(): Promise; + + getAccessTokenExpirationTime(): Promise; + + getRefreshTokenExpirationTime(rememberMe?: boolean): Promise; + + getIssuer(): Promise; + + getAudience(): Promise; + + getSubject(): Promise; + + getPayloadEncryption(): Promise; + + getPermissionTokenExpirationTime(): Promise; +} diff --git a/src/common/auth/services/auth.service.ts b/src/common/auth/services/auth.service.ts new file mode 100644 index 0000000..2d8c938 --- /dev/null +++ b/src/common/auth/services/auth.service.ts @@ -0,0 +1,371 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IAuthPassword, + IAuthPayloadOptions, + IAuthRefreshTokenOptions, +} from 'src/common/auth/interfaces/auth.interface'; +import { IAuthService } from 'src/common/auth/interfaces/auth.service.interface'; +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'; + +@Injectable() +export class AuthService implements IAuthService { + private readonly accessTokenSecretKey: string; + private readonly accessTokenExpirationTime: number; + private readonly accessTokenNotBeforeExpirationTime: number; + private readonly accessTokenEncryptKey: string; + private readonly accessTokenEncryptIv: string; + + private readonly refreshTokenSecretKey: string; + private readonly refreshTokenExpirationTime: number; + private readonly refreshTokenExpirationTimeRememberMe: number; + private readonly refreshTokenNotBeforeExpirationTime: number; + private readonly refreshTokenEncryptKey: string; + private readonly refreshTokenEncryptIv: string; + + private readonly payloadEncryption: boolean; + private readonly prefixAuthorization: string; + private readonly audience: string; + private readonly issuer: string; + private readonly subject: string; + + private readonly passwordExpiredIn: number; + private readonly passwordSaltLength: number; + + private readonly permissionTokenSecretToken: string; + private readonly permissionTokenExpirationTime: number; + private readonly permissionTokenNotBeforeExpirationTime: number; + private readonly permissionTokenEncryptKey: string; + private readonly permissionTokenEncryptIv: string; + + constructor( + private readonly helperHashService: HelperHashService, + private readonly helperDateService: HelperDateService, + private readonly helperEncryptionService: HelperEncryptionService, + private readonly configService: ConfigService + ) { + this.accessTokenSecretKey = this.configService.get( + 'auth.accessToken.secretKey' + ); + this.accessTokenExpirationTime = this.configService.get( + 'auth.accessToken.expirationTime' + ); + this.accessTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.accessToken.notBeforeExpirationTime' + ); + this.accessTokenEncryptKey = this.configService.get( + 'auth.accessToken.encryptKey' + ); + this.accessTokenEncryptIv = this.configService.get( + 'auth.accessToken.encryptIv' + ); + + this.refreshTokenSecretKey = this.configService.get( + 'auth.refreshToken.secretKey' + ); + this.refreshTokenExpirationTime = this.configService.get( + 'auth.refreshToken.expirationTime' + ); + this.refreshTokenExpirationTimeRememberMe = + this.configService.get( + 'auth.refreshToken.expirationTimeRememberMe' + ); + this.refreshTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.refreshToken.notBeforeExpirationTime' + ); + this.refreshTokenEncryptKey = this.configService.get( + 'auth.refreshToken.encryptKey' + ); + this.refreshTokenEncryptIv = this.configService.get( + 'auth.refreshToken.encryptIv' + ); + + this.payloadEncryption = this.configService.get( + 'auth.payloadEncryption' + ); + this.prefixAuthorization = this.configService.get( + 'auth.prefixAuthorization' + ); + this.subject = this.configService.get('auth.subject'); + this.audience = this.configService.get('auth.audience'); + this.issuer = this.configService.get('auth.issuer'); + + this.passwordExpiredIn = this.configService.get( + 'auth.password.expiredIn' + ); + this.passwordSaltLength = this.configService.get( + 'auth.password.saltLength' + ); + + this.permissionTokenSecretToken = this.configService.get( + 'auth.permissionToken.secretKey' + ); + this.permissionTokenExpirationTime = this.configService.get( + 'auth.permissionToken.expirationTime' + ); + this.permissionTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.permissionToken.notBeforeExpirationTime' + ); + this.permissionTokenEncryptKey = this.configService.get( + 'auth.permissionToken.encryptKey' + ); + this.permissionTokenEncryptIv = this.configService.get( + 'auth.permissionToken.encryptIv' + ); + } + + async createSalt(length: number): Promise { + return this.helperHashService.randomSalt(length); + } + + async encryptAccessToken(payload: Record): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.accessTokenEncryptKey, + this.accessTokenEncryptIv + ); + } + + async decryptAccessToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.accessTokenEncryptKey, + this.accessTokenEncryptIv + ) as Record; + } + + async createAccessToken( + payloadHashed: string | Record + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.accessTokenSecretKey, + expiredIn: this.accessTokenExpirationTime, + notBefore: this.accessTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validateAccessToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.accessTokenSecretKey, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadAccessToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async encryptRefreshToken(payload: Record): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.refreshTokenEncryptKey, + this.refreshTokenEncryptIv + ); + } + + async decryptRefreshToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.refreshTokenEncryptKey, + this.refreshTokenEncryptIv + ) as Record; + } + + async createRefreshToken( + payloadHashed: string | Record, + options?: IAuthRefreshTokenOptions + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.refreshTokenSecretKey, + expiredIn: options?.rememberMe + ? this.refreshTokenExpirationTimeRememberMe + : this.refreshTokenExpirationTime, + notBefore: + options?.notBeforeExpirationTime ?? + this.refreshTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validateRefreshToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.refreshTokenSecretKey, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadRefreshToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async encryptPermissionToken( + payload: Record + ): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.permissionTokenEncryptKey, + this.permissionTokenEncryptIv + ); + } + + async decryptPermissionToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.permissionTokenEncryptKey, + this.permissionTokenEncryptIv + ) as Record; + } + + async createPermissionToken( + payloadHashed: string | Record + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.permissionTokenSecretToken, + expiredIn: this.permissionTokenExpirationTime, + notBefore: this.permissionTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validatePermissionToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.permissionTokenSecretToken, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadPermissionToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async validateUser( + passwordString: string, + passwordHash: string + ): Promise { + return this.helperHashService.bcryptCompare( + passwordString, + passwordHash + ); + } + + async createPayloadAccessToken( + data: Record, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise> { + return { + ...data, + rememberMe, + loginDate: options?.loginDate ?? this.helperDateService.create(), + }; + } + + async createPayloadRefreshToken( + _id: string, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise> { + return { + _id, + rememberMe, + loginDate: options?.loginDate, + }; + } + + async createPayloadPermissionToken( + data: Record + ): Promise> { + return data; + } + + async createPassword(password: string): Promise { + const salt: string = this.helperHashService.randomSalt(20); + + const passwordExpired: Date = this.helperDateService.forwardInSeconds( + this.passwordExpiredIn + ); + const passwordCreated: Date = this.helperDateService.create(); + const passwordHash = this.helperHashService.bcrypt(password, salt); + return { + passwordHash, + passwordExpired, + passwordCreated, + salt, + }; + } + + async checkPasswordExpired(passwordExpired: Date): Promise { + const today: Date = this.helperDateService.create(); + const passwordExpiredConvert: Date = + this.helperDateService.create(passwordExpired); + + return today > passwordExpiredConvert; + } + + async getTokenType(): Promise { + return this.prefixAuthorization; + } + + async getAccessTokenExpirationTime(): Promise { + return this.accessTokenExpirationTime; + } + + async getRefreshTokenExpirationTime(rememberMe?: boolean): Promise { + return rememberMe + ? this.refreshTokenExpirationTimeRememberMe + : this.refreshTokenExpirationTime; + } + + async getIssuer(): Promise { + return this.issuer; + } + + async getAudience(): Promise { + return this.audience; + } + + async getSubject(): Promise { + return this.subject; + } + + async getPayloadEncryption(): Promise { + return this.payloadEncryption; + } + + async getPermissionTokenExpirationTime(): Promise { + return this.permissionTokenExpirationTime; + } +} diff --git a/src/common/aws/aws.module.ts b/src/common/aws/aws.module.ts new file mode 100644 index 0000000..bcd9794 --- /dev/null +++ b/src/common/aws/aws.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AwsS3Service } from './services/aws.s3.service'; + +@Module({ + exports: [AwsS3Service], + providers: [AwsS3Service], + imports: [], + controllers: [], +}) +export class AwsModule {} diff --git a/src/common/aws/constants/aws.s3.constant.ts b/src/common/aws/constants/aws.s3.constant.ts new file mode 100644 index 0000000..23ac4a9 --- /dev/null +++ b/src/common/aws/constants/aws.s3.constant.ts @@ -0,0 +1 @@ +export const AwsS3MaxPartNumber = 10000; diff --git a/src/common/aws/interfaces/aws.interface.ts b/src/common/aws/interfaces/aws.interface.ts new file mode 100644 index 0000000..e907cbf --- /dev/null +++ b/src/common/aws/interfaces/aws.interface.ts @@ -0,0 +1,6 @@ +import { ObjectCannedACL } from '@aws-sdk/client-s3'; + +export interface IAwsS3PutItemOptions { + path: string; + acl?: ObjectCannedACL; +} diff --git a/src/common/aws/interfaces/aws.s3-service.interface.ts b/src/common/aws/interfaces/aws.s3-service.interface.ts new file mode 100644 index 0000000..ad417aa --- /dev/null +++ b/src/common/aws/interfaces/aws.s3-service.interface.ts @@ -0,0 +1,63 @@ +import { + CompletedPart, + HeadBucketCommandOutput, + UploadPartRequest, +} from '@aws-sdk/client-s3'; +import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface'; +import { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { Readable } from 'stream'; + +export interface IAwsS3Service { + checkConnection(): Promise; + + listBucket(): Promise; + + listItemInBucket(prefix?: string): Promise; + + getItemInBucket( + filename: string, + path?: string + ): Promise | Blob>; + + putItemInBucket( + filename: string, + content: + | string + | Uint8Array + | Buffer + | Readable + | ReadableStream + | Blob, + options?: IAwsS3PutItemOptions + ): Promise; + + deleteItemInBucket(filename: string): Promise; + + deleteItemsInBucket(filenames: string[]): Promise; + + deleteFolder(dir: string): Promise; + + createMultiPart( + filename: string, + options?: IAwsS3PutItemOptions + ): Promise; + + uploadPart( + path: string, + content: UploadPartRequest['Body'] | string | Uint8Array | Buffer, + uploadId: string, + partNumber: number + ): Promise; + + completeMultipart( + path: string, + uploadId: string, + parts: CompletedPart[] + ): Promise; + + abortMultipart(path: string, uploadId: string): Promise; +} diff --git a/src/common/aws/serializations/aws.s3-multipart.serialization.ts b/src/common/aws/serializations/aws.s3-multipart.serialization.ts new file mode 100644 index 0000000..af29cb8 --- /dev/null +++ b/src/common/aws/serializations/aws.s3-multipart.serialization.ts @@ -0,0 +1,53 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; + +export class AwsS3MultipartPartsSerialization { + @ApiProperty({ + example: faker.random.alpha(10), + description: 'ETag from aws after init multipart', + }) + @Type(() => String) + ETag: string; + + @ApiProperty({ + example: 1, + }) + @Type(() => Number) + PartNumber: number; +} + +export class AwsS3MultipartSerialization extends AwsS3Serialization { + @ApiProperty({ + example: faker.random.alpha(20), + description: 'Upload id from aws after init multipart', + }) + @Type(() => String) + uploadId: string; + + @ApiProperty({ + example: 1, + description: 'Last part number uploaded', + }) + @Type(() => Number) + partNumber?: number; + + @ApiProperty({ + example: 200, + description: 'Max part number, or length of the chunk', + }) + @Type(() => Number) + maxPartNumber?: number; + + @ApiProperty({ + oneOf: [ + { + $ref: getSchemaPath(AwsS3MultipartPartsSerialization), + type: 'array', + }, + ], + }) + @Type(() => AwsS3MultipartPartsSerialization) + parts?: AwsS3MultipartPartsSerialization[]; +} diff --git a/src/common/aws/serializations/aws.s3.serialization.ts b/src/common/aws/serializations/aws.s3.serialization.ts new file mode 100644 index 0000000..9440ebf --- /dev/null +++ b/src/common/aws/serializations/aws.s3.serialization.ts @@ -0,0 +1,41 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class AwsS3Serialization { + @ApiProperty({ + example: faker.system.directoryPath(), + }) + @Type(() => String) + path: string; + + @ApiProperty({ + example: faker.system.filePath(), + }) + @Type(() => String) + pathWithFilename: string; + + @ApiProperty({ + example: faker.system.fileName(), + }) + @Type(() => String) + filename: string; + + @ApiProperty({ + example: `${faker.internet.url()}/${faker.system.filePath()}`, + }) + @Type(() => String) + completedUrl: string; + + @ApiProperty({ + example: faker.internet.url(), + }) + @Type(() => String) + baseUrl: string; + + @ApiProperty({ + example: faker.system.mimeType(), + }) + @Type(() => String) + mime: string; +} diff --git a/src/common/aws/services/aws.s3.service.ts b/src/common/aws/services/aws.s3.service.ts new file mode 100644 index 0000000..fc36121 --- /dev/null +++ b/src/common/aws/services/aws.s3.service.ts @@ -0,0 +1,377 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface'; +import { IAwsS3Service } from 'src/common/aws/interfaces/aws.s3-service.interface'; +import { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { Readable } from 'stream'; +import { + S3Client, + GetObjectCommand, + ListBucketsCommand, + ListObjectsV2Command, + PutObjectCommand, + DeleteObjectCommand, + DeleteObjectsCommand, + ObjectIdentifier, + CreateMultipartUploadCommand, + CreateMultipartUploadCommandInput, + UploadPartCommandInput, + UploadPartCommand, + CompleteMultipartUploadCommandInput, + CompleteMultipartUploadCommand, + CompletedPart, + GetObjectCommandInput, + AbortMultipartUploadCommand, + AbortMultipartUploadCommandInput, + UploadPartRequest, + HeadBucketCommand, + HeadBucketCommandOutput, + ListBucketsOutput, + Bucket, + ListObjectsV2Output, + _Object, + GetObjectOutput, +} from '@aws-sdk/client-s3'; + +@Injectable() +export class AwsS3Service implements IAwsS3Service { + private readonly s3Client: S3Client; + private readonly bucket: string; + private readonly baseUrl: string; + + constructor(private readonly configService: ConfigService) { + this.s3Client = new S3Client({ + credentials: { + accessKeyId: + this.configService.get('aws.credential.key'), + secretAccessKey: this.configService.get( + 'aws.credential.secret' + ), + }, + region: this.configService.get('aws.s3.region'), + }); + + this.bucket = this.configService.get('aws.s3.bucket'); + this.baseUrl = this.configService.get('aws.s3.baseUrl'); + } + + async checkConnection(): Promise { + const command: HeadBucketCommand = new HeadBucketCommand({ + Bucket: this.bucket, + }); + + try { + const check: HeadBucketCommandOutput = await this.s3Client.send( + command + ); + + return check; + } catch (err: any) { + throw err; + } + } + + async listBucket(): Promise { + const command: ListBucketsCommand = new ListBucketsCommand({}); + + try { + const listBucket: ListBucketsOutput = await this.s3Client.send( + command + ); + const mapList = listBucket.Buckets.map((val: Bucket) => val.Name); + + return mapList; + } catch (err: any) { + throw err; + } + } + + async listItemInBucket(prefix?: string): Promise { + const command: ListObjectsV2Command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: prefix, + }); + + try { + const listItems: ListObjectsV2Output = await this.s3Client.send( + command + ); + + const mapList = listItems.Contents.map((val: _Object) => { + const lastIndex: number = val.Key.lastIndexOf('/'); + const path: string = val.Key.substring(0, lastIndex); + const filename: string = val.Key.substring( + lastIndex, + val.Key.length + ); + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toLocaleUpperCase(); + + return { + path, + pathWithFilename: val.Key, + filename: filename, + completedUrl: `${this.baseUrl}/${val.Key}`, + baseUrl: this.baseUrl, + mime, + }; + }); + + return mapList; + } catch (err: any) { + throw err; + } + } + + async getItemInBucket( + filename: string, + path?: string + ): Promise | Blob> { + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const key: string = path ? `${path}/${filename}` : filename; + const input: GetObjectCommandInput = { + Bucket: this.bucket, + Key: key, + }; + const command: GetObjectCommand = new GetObjectCommand(input); + + try { + const item: GetObjectOutput = await this.s3Client.send(command); + + return item.Body; + } catch (err: any) { + throw err; + } + } + + async putItemInBucket( + filename: string, + content: + | string + | Uint8Array + | Buffer + | Readable + | ReadableStream + | Blob, + options?: IAwsS3PutItemOptions + ): Promise { + let path: string = options?.path; + const acl: string = options?.acl ? options.acl : 'public-read'; + + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + const key: string = path ? `${path}/${filename}` : filename; + const command: PutObjectCommand = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: content, + ACL: acl, + }); + + try { + await this.s3Client.send(command); + } catch (err: any) { + throw err; + } + + return { + path, + pathWithFilename: key, + filename: filename, + completedUrl: `${this.baseUrl}/${key}`, + baseUrl: this.baseUrl, + mime, + }; + } + + async deleteItemInBucket(filename: string): Promise { + const command: DeleteObjectCommand = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: filename, + }); + + try { + await this.s3Client.send(command); + return; + } catch (err: any) { + throw err; + } + } + + async deleteItemsInBucket(filenames: string[]): Promise { + const keys: ObjectIdentifier[] = filenames.map((val) => ({ + Key: val, + })); + const command: DeleteObjectsCommand = new DeleteObjectsCommand({ + Bucket: this.bucket, + Delete: { + Objects: keys, + }, + }); + + try { + await this.s3Client.send(command); + return; + } catch (err: any) { + throw err; + } + } + + async deleteFolder(dir: string): Promise { + const commandList: ListObjectsV2Command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: dir, + }); + const lists = await this.s3Client.send(commandList); + + try { + const listItems = lists.Contents.map((val) => ({ + Key: val.Key, + })); + const commandDeleteItems: DeleteObjectsCommand = + new DeleteObjectsCommand({ + Bucket: this.bucket, + Delete: { + Objects: listItems, + }, + }); + + await this.s3Client.send(commandDeleteItems); + + const commandDelete: DeleteObjectCommand = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: dir, + }); + await this.s3Client.send(commandDelete); + + return; + } catch (err: any) { + throw err; + } + } + + async createMultiPart( + filename: string, + options?: IAwsS3PutItemOptions + ): Promise { + let path: string = options?.path; + const acl: string = options?.acl ? options.acl : 'public-read'; + + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + const key: string = path ? `${path}/${filename}` : filename; + + const multiPartInput: CreateMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: key, + ACL: acl, + }; + const multiPartCommand: CreateMultipartUploadCommand = + new CreateMultipartUploadCommand(multiPartInput); + + try { + const response = await this.s3Client.send(multiPartCommand); + + return { + uploadId: response.UploadId, + path, + pathWithFilename: key, + filename: filename, + completedUrl: `${this.baseUrl}/${key}`, + baseUrl: this.baseUrl, + mime, + }; + } catch (err: any) { + throw err; + } + } + + async uploadPart( + path: string, + content: UploadPartRequest['Body'] | string | Uint8Array | Buffer, + uploadId: string, + partNumber: number + ): Promise { + const uploadPartInput: UploadPartCommandInput = { + Bucket: this.bucket, + Key: path, + Body: content, + PartNumber: partNumber, + UploadId: uploadId, + }; + const uploadPartCommand: UploadPartCommand = new UploadPartCommand( + uploadPartInput + ); + + try { + const { ETag } = await this.s3Client.send(uploadPartCommand); + + return { + ETag, + PartNumber: partNumber, + }; + } catch (err: any) { + throw err; + } + } + + async completeMultipart( + path: string, + uploadId: string, + parts: CompletedPart[] + ): Promise { + const completeMultipartInput: CompleteMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: path, + UploadId: uploadId, + MultipartUpload: { + Parts: parts, + }, + }; + + const completeMultipartCommand: CompleteMultipartUploadCommand = + new CompleteMultipartUploadCommand(completeMultipartInput); + + try { + await this.s3Client.send(completeMultipartCommand); + + return; + } catch (err: any) { + throw err; + } + } + + async abortMultipart(path: string, uploadId: string): Promise { + const abortMultipartInput: AbortMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: path, + UploadId: uploadId, + }; + + const abortMultipartCommand: AbortMultipartUploadCommand = + new AbortMultipartUploadCommand(abortMultipartInput); + + try { + await this.s3Client.send(abortMultipartCommand); + + return; + } catch (err: any) { + throw err; + } + } +} diff --git a/src/common/common.module.ts b/src/common/common.module.ts new file mode 100644 index 0000000..f8b6095 --- /dev/null +++ b/src/common/common.module.ts @@ -0,0 +1,179 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DebuggerModule } from 'src/common/debugger/debugger.module'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ErrorModule } from 'src/common/error/error.module'; +import { ResponseModule } from 'src/common/response/response.module'; +import { RequestModule } from 'src/common/request/request.module'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { MessageModule } from 'src/common/message/message.module'; +import { LoggerModule } from 'src/common/logger/logger.module'; +import { PaginationModule } from 'src/common/pagination/pagination.module'; +import Joi from 'joi'; +import { ENUM_MESSAGE_LANGUAGE } from './message/constants/message.enum.constant'; +import configs from 'src/configs'; +import { SettingModule } from 'src/common/setting/setting.module'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; + +@Module({ + controllers: [], + providers: [], + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + validationSchema: Joi.object({ + APP_NAME: Joi.string().required(), + APP_ENV: Joi.string() + .valid(...Object.values(ENUM_APP_ENVIRONMENT)) + .default('development') + .required(), + APP_LANGUAGE: Joi.string() + .valid(...Object.values(ENUM_MESSAGE_LANGUAGE)) + .default(APP_LANGUAGE) + .required(), + + HTTP_ENABLE: Joi.boolean().default(true).required(), + HTTP_HOST: [ + Joi.string().ip({ version: 'ipv4' }).required(), + Joi.valid('localhost').required(), + ], + HTTP_PORT: Joi.number().default(3000).required(), + HTTP_VERSIONING_ENABLE: Joi.boolean().default(true).required(), + HTTP_VERSION: Joi.number().required(), + + DEBUGGER_HTTP_WRITE_INTO_FILE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_HTTP_WRITE_INTO_CONSOLE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_SYSTEM_WRITE_INTO_FILE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE: Joi.boolean() + .default(false) + .required(), + + JOB_ENABLE: Joi.boolean().default(false).required(), + + DATABASE_HOST: Joi.string() + .default('mongodb://localhost:27017') + .required(), + DATABASE_NAME: Joi.string().default('ack').required(), + DATABASE_USER: Joi.string().allow(null, '').optional(), + DATABASE_PASSWORD: Joi.string().allow(null, '').optional(), + DATABASE_DEBUG: Joi.boolean().default(false).required(), + DATABASE_OPTIONS: Joi.string().allow(null, '').optional(), + + AUTH_JWT_SUBJECT: Joi.string().required(), + AUTH_JWT_AUDIENCE: Joi.string().required(), + AUTH_JWT_ISSUER: Joi.string().required(), + + AUTH_JWT_ACCESS_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_JWT_ACCESS_TOKEN_EXPIRED: Joi.string() + .default('15m') + .required(), + + AUTH_JWT_REFRESH_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_JWT_REFRESH_TOKEN_EXPIRED: Joi.string() + .default('7d') + .required(), + AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED: Joi.string() + .default('30d') + .required(), + AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION: Joi.string() + .default('15m') + .required(), + + AUTH_PERMISSION_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_PERMISSION_TOKEN_EXPIRED: Joi.string() + .default('5m') + .required(), + + AUTH_JWT_PAYLOAD_ENCRYPT: Joi.boolean() + .default(false) + .required(), + AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + + AWS_CREDENTIAL_KEY: Joi.string().allow(null, '').optional(), + AWS_CREDENTIAL_SECRET: Joi.string().allow(null, '').optional(), + AWS_S3_REGION: Joi.string().allow(null, '').optional(), + AWS_S3_BUCKET: Joi.string().allow(null, '').optional(), + }), + validationOptions: { + allowUnknown: true, + abortEarly: true, + }, + }), + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: (databaseOptionsService: DatabaseOptionsService) => + databaseOptionsService.createOptions(), + }), + MessageModule, + HelperModule, + PaginationModule, + ErrorModule, + DebuggerModule.forRoot(), + ResponseModule, + RequestModule, + SettingModule, + LoggerModule, + ApiKeyModule, + AuthModule, + ], +}) +export class CommonModule {} diff --git a/src/common/dashboard/constants/dashboard.doc.constant.ts b/src/common/dashboard/constants/dashboard.doc.constant.ts new file mode 100644 index 0000000..b0d4036 --- /dev/null +++ b/src/common/dashboard/constants/dashboard.doc.constant.ts @@ -0,0 +1,21 @@ +import { faker } from '@faker-js/faker'; + +export const DashboardDocQueryStartDate = [ + { + name: 'startDate', + allowEmptyValue: true, + required: false, + type: 'string', + example: faker.date.recent().toString(), + }, +]; + +export const DashboardDocQueryEndDate = [ + { + name: 'endDate', + allowEmptyValue: true, + required: false, + type: 'string', + example: faker.date.recent().toString(), + }, +]; diff --git a/src/common/dashboard/dashboard.module.ts b/src/common/dashboard/dashboard.module.ts new file mode 100644 index 0000000..1b2ebfe --- /dev/null +++ b/src/common/dashboard/dashboard.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DashboardService } from 'src/common/dashboard/services/dashboard.service'; + +@Module({ + controllers: [], + providers: [DashboardService], + exports: [DashboardService], + imports: [], +}) +export class DashboardModule {} diff --git a/src/common/dashboard/dtos/dashboard.ts b/src/common/dashboard/dtos/dashboard.ts new file mode 100644 index 0000000..49b4a7a --- /dev/null +++ b/src/common/dashboard/dtos/dashboard.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDate, IsOptional, ValidateIf } from 'class-validator'; +import { MinGreaterThan } from 'src/common/request/validations/request.min-greater-than.validation'; + +export class DashboardDto { + @ApiProperty({ + name: 'startDate', + required: false, + nullable: true, + }) + @IsDate() + @IsOptional() + @Type(() => Date) + @ValidateIf((e) => e.startDate !== '' || e.endDate !== '') + startDate?: Date; + + @ApiProperty({ + name: 'endDate', + required: false, + nullable: true, + }) + @IsDate() + @IsOptional() + @MinGreaterThan('startDate') + @Type(() => Date) + @ValidateIf((e) => e.startDate !== '' || e.endDate !== '') + endDate?: Date; +} diff --git a/src/common/dashboard/interfaces/dashboard.interface.ts b/src/common/dashboard/interfaces/dashboard.interface.ts new file mode 100644 index 0000000..f206b7a --- /dev/null +++ b/src/common/dashboard/interfaces/dashboard.interface.ts @@ -0,0 +1,18 @@ +export interface IDashboardStartAndEndDate { + startDate: Date; + endDate: Date; +} + +export interface IDashboardStartAndEndYear { + startYear: number; + endYear: number; +} + +export interface IDashboardStartAndEnd { + month: number; + year: number; +} + +export interface IDashboardMonthAndYear extends Partial { + total: number; +} diff --git a/src/common/dashboard/interfaces/dashboard.service.interface.ts b/src/common/dashboard/interfaces/dashboard.service.interface.ts new file mode 100644 index 0000000..272cd0d --- /dev/null +++ b/src/common/dashboard/interfaces/dashboard.service.interface.ts @@ -0,0 +1,24 @@ +import { DashboardDto } from 'src/common/dashboard/dtos/dashboard'; +import { + IDashboardStartAndEnd, + IDashboardStartAndEndDate, + IDashboardStartAndEndYear, +} from 'src/common/dashboard/interfaces/dashboard.interface'; + +export interface IDashboardService { + getStartAndEndDate(date: DashboardDto): Promise; + + getMonths(): Promise; + + getStartAndEndYear({ + startDate, + endDate, + }: IDashboardStartAndEndDate): Promise; + + getStartAndEndMonth({ + month, + year, + }: IDashboardStartAndEnd): Promise; + + getPercentage(value: number, total: number): Promise; +} diff --git a/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts b/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts new file mode 100644 index 0000000..7bb0cc9 --- /dev/null +++ b/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts @@ -0,0 +1,38 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; + +export class DashboardMonthAndYearSerialization { + @ApiProperty({ + name: 'total', + example: 12, + }) + month: number; + + @ApiProperty({ + name: 'total', + example: 2001, + }) + year: number; + + @ApiProperty({ + name: 'total', + description: 'total of target', + required: true, + nullable: false, + example: 0, + }) + total: number; +} + +export class DashboardMonthAndYearPercentageSerialization extends OmitType( + DashboardMonthAndYearSerialization, + ['total'] as const +) { + @ApiProperty({ + name: 'percent', + description: 'Percent of target', + required: true, + nullable: false, + example: 15.4, + }) + percent: number; +} diff --git a/src/common/dashboard/serializations/dashboard.serialization.ts b/src/common/dashboard/serializations/dashboard.serialization.ts new file mode 100644 index 0000000..c92f0e0 --- /dev/null +++ b/src/common/dashboard/serializations/dashboard.serialization.ts @@ -0,0 +1,12 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DashboardSerialization { + @ApiProperty({ + name: 'total', + example: faker.random.numeric(4, { allowLeadingZeros: false }), + description: 'Total user', + nullable: false, + }) + total: number; +} diff --git a/src/common/dashboard/services/dashboard.service.ts b/src/common/dashboard/services/dashboard.service.ts new file mode 100644 index 0000000..2d14d9f --- /dev/null +++ b/src/common/dashboard/services/dashboard.service.ts @@ -0,0 +1,81 @@ +import { DashboardDto } from 'src/common/dashboard/dtos/dashboard'; +import { + IDashboardStartAndEnd, + IDashboardStartAndEndDate, + IDashboardStartAndEndYear, +} from 'src/common/dashboard/interfaces/dashboard.interface'; +import { IDashboardService } from 'src/common/dashboard/interfaces/dashboard.service.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; + +export class DashboardService implements IDashboardService { + constructor( + private readonly helperDateService: HelperDateService, + private readonly helperNumberService: HelperNumberService + ) {} + + async getStartAndEndDate( + date: DashboardDto + ): Promise { + const today = this.helperDateService.create(); + let { startDate, endDate } = date; + + if (!startDate && !endDate) { + startDate = this.helperDateService.startOfYear(today); + endDate = this.helperDateService.endOfYear(today); + } else { + if (!startDate) { + startDate = this.helperDateService.startOfDay(); + } else { + startDate = this.helperDateService.startOfDay(startDate); + } + + if (!endDate) { + endDate = this.helperDateService.endOfDay(); + } else { + endDate = this.helperDateService.endOfDay(endDate); + } + } + + return { + startDate, + endDate, + }; + } + + async getMonths(): Promise { + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + } + + async getStartAndEndYear({ + startDate, + endDate, + }: IDashboardStartAndEndDate): Promise { + return { + startYear: startDate.getFullYear(), + endYear: endDate.getFullYear(), + }; + } + + async getStartAndEndMonth({ + month, + year, + }: IDashboardStartAndEnd): Promise { + const monthString = `${month}`.padStart(2, '0'); + const date: Date = this.helperDateService.create( + `${year}-${monthString}-01` + ); + + const startDate = this.helperDateService.startOfMonth(date); + const endDate = this.helperDateService.endOfMonth(date); + + return { + startDate, + endDate, + }; + } + + async getPercentage(value: number, total: number): Promise { + return this.helperNumberService.percent(value, total); + } +} diff --git a/src/common/database/abstracts/database.base-entity.abstract.ts b/src/common/database/abstracts/database.base-entity.abstract.ts new file mode 100644 index 0000000..31c0b66 --- /dev/null +++ b/src/common/database/abstracts/database.base-entity.abstract.ts @@ -0,0 +1,12 @@ +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 DatabaseBaseEntityAbstract { + 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/database.base-repository.abstract.ts b/src/common/database/abstracts/database.base-repository.abstract.ts new file mode 100644 index 0000000..5739585 --- /dev/null +++ b/src/common/database/abstracts/database.base-repository.abstract.ts @@ -0,0 +1,116 @@ +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseBaseRepositoryAbstract { + 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?: IDatabaseFindOneOptions + ): Promise; + + abstract findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + abstract getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + abstract exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise; + + abstract create( + data: Dto, + options?: IDatabaseCreateOptions + ): Promise; + + abstract save(repository: Entity): Promise; + + abstract delete(repository: Entity): Promise; + + abstract softDelete(repository: Entity): Promise; + + abstract restore(repository: Entity): 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 raw( + rawOperation: RawQuery, + options?: IDatabaseRawOptions + ): Promise; + + abstract model(): Promise; +} diff --git a/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts b/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts new file mode 100644 index 0000000..294116a --- /dev/null +++ b/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts @@ -0,0 +1,38 @@ +import { Prop } from '@nestjs/mongoose'; +import { Types } from 'mongoose'; +import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-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 { DatabaseDefaultObjectId } from 'src/common/database/constants/database.function.constant'; + +export abstract class DatabaseMongoObjectIdEntityAbstract extends DatabaseBaseEntityAbstract { + @Prop({ + type: Types.ObjectId, + default: DatabaseDefaultObjectId, + }) + _id: Types.ObjectId; + + @Prop({ + required: false, + index: true, + type: Date, + }) + [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'asc', + type: Date, + }) + [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'desc', + type: Date, + }) + [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; +} diff --git a/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts b/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts new file mode 100644 index 0000000..7acdb5f --- /dev/null +++ b/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts @@ -0,0 +1,37 @@ +import { Prop } from '@nestjs/mongoose'; +import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-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 { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseBaseEntityAbstract { + @Prop({ + type: String, + default: DatabaseDefaultUUID, + }) + _id: string; + + @Prop({ + required: false, + index: true, + type: Date, + }) + [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'asc', + type: Date, + }) + [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'desc', + type: Date, + }) + [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; +} diff --git a/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts b/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts new file mode 100644 index 0000000..2e824e5 --- /dev/null +++ b/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts @@ -0,0 +1,746 @@ +import { + ClientSession, + Model, + PipelineStage, + PopulateOptions, + Types, + Document, +} from 'mongoose'; +import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract'; +import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseMongoObjectIdRepositoryAbstract< + Entity, + EntityDocument +> extends DatabaseBaseRepositoryAbstract { + protected _repository: Model; + protected _joinOnFind?: PopulateOptions | PopulateOptions[]; + + constructor( + repository: Model, + options?: PopulateOptions | PopulateOptions[] + ) { + super(); + + this._repository = repository; + this._joinOnFind = options; + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.find(find); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + + async findAllDistinct( + fieldDistinct: string, + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.distinct( + fieldDistinct, + find + ); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOne(find); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findById( + new Types.ObjectId(_id) + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneAndLock( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOneAndUpdate( + find, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as T; + } + + async findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findByIdAndUpdate( + new Types.ObjectId(_id), + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as T; + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + const count = this._repository.countDocuments(find); + + if (options?.withDeleted) { + count.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + return count; + } + + async exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise { + + if (options?.excludeId) { + find= { + ...find, + _id: { + $nin: options?.excludeId.map( + (val) => new Types.ObjectId(val) + ) ?? [], + } + } + } + + const exist = this._repository.exists(find); + + if (options?.withDeleted) { + exist.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + const result = await exist; + return result ? true : false; + } + + async create( + data: Dto, + options?: IDatabaseCreateOptions + ): Promise { + const dataCreate: Record = data; + dataCreate._id = new Types.ObjectId(options?._id); + + const created = await this._repository.create([dataCreate], { + session: options ? options.session : undefined, + }); + + return created[0] as EntityDocument; + } + + async save( + repository: EntityDocument & Document + ): Promise { + return repository.save(); + } + + async delete( + repository: EntityDocument & Document + ): Promise { + return repository.deleteOne(); + } + + async softDelete( + repository: EntityDocument & + Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = new Date(); + return repository.save(); + } + + async restore( + repository: EntityDocument & + Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = undefined; + return repository.save(); + } + + // bulk + async createMany( + data: Dto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const dataCreate: Record[] = data.map( + (val: Record) => ({ + ...val, + _id: new Types.ObjectId(val._id), + }) + ); + + const create = this._repository.insertMany(dataCreate, { + 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.map((val) => new Types.ObjectId(val)), + }, + }); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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.map((val) => new Types.ObjectId(val)), + }, + }, + { + $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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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.map((val) => new Types.ObjectId(val)), + }, + }, + { + $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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await update; + return true; + } catch (err: unknown) { + throw err; + } + } + + // raw + 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: { + $or: [ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { + $exists: false, + }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ], + }, + }); + } else { + 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 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 new file mode 100644 index 0000000..5a253a5 --- /dev/null +++ b/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts @@ -0,0 +1,734 @@ +import { + ClientSession, + Model, + PipelineStage, + PopulateOptions, + Document, +} from 'mongoose'; +import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract'; +import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseMongoUUIDRepositoryAbstract< + Entity, + EntityDocument +> extends DatabaseBaseRepositoryAbstract { + protected _repository: Model; + protected _joinOnFind?: PopulateOptions | PopulateOptions[]; + + constructor( + repository: Model, + options?: PopulateOptions | PopulateOptions[] + ) { + super(); + + this._repository = repository; + this._joinOnFind = options; + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.find(find); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + + return findAll.lean() as any; + } + + async findAllDistinct( + fieldDistinct: string, + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.distinct( + fieldDistinct, + find + ); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOne(find); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findById(_id); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + + return findOne.exec() as any; + } + + async findOneAndLock( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOneAndUpdate( + find, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findByIdAndUpdate( + _id, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + const count = this._repository.countDocuments(find); + + if (options?.withDeleted) { + count.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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 + ): Promise { + return repository.save(); + } + + async delete( + repository: EntityDocument & Document + ): Promise { + return repository.deleteOne(); + } + + async softDelete( + repository: EntityDocument & Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = new Date(); + return repository.save(); + } + + async restore( + repository: EntityDocument & Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = undefined; + return repository.save(); + } + + // 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, + }, + }); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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, + }, + }, + { + $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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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, + }, + }, + { + $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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + 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: { + $or: [ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { + $exists: false, + }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ], + }, + }); + } else { + 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 model(): Promise> { + return this._repository; + } +} diff --git a/src/common/database/constants/database.constant.ts b/src/common/database/constants/database.constant.ts new file mode 100644 index 0000000..3a992e2 --- /dev/null +++ b/src/common/database/constants/database.constant.ts @@ -0,0 +1,5 @@ +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/constants/database.function.constant.ts b/src/common/database/constants/database.function.constant.ts new file mode 100644 index 0000000..3dcaaa9 --- /dev/null +++ b/src/common/database/constants/database.function.constant.ts @@ -0,0 +1,6 @@ +import { Types } from 'mongoose'; +import { v4 as uuidV4 } from 'uuid'; + +export const DatabaseDefaultUUID = uuidV4; + +export const DatabaseDefaultObjectId = () => new Types.ObjectId(); diff --git a/src/common/database/database.options.module.ts b/src/common/database/database.options.module.ts new file mode 100644 index 0000000..2913972 --- /dev/null +++ b/src/common/database/database.options.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; + +@Module({ + providers: [DatabaseOptionsService], + exports: [DatabaseOptionsService], + imports: [], + controllers: [], +}) +export class DatabaseOptionsModule {} diff --git a/src/common/database/decorators/database.decorator.ts b/src/common/database/decorators/database.decorator.ts new file mode 100644 index 0000000..3cfbc8a --- /dev/null +++ b/src/common/database/decorators/database.decorator.ts @@ -0,0 +1,35 @@ +import { + InjectConnection, + InjectModel, + Schema, + SchemaOptions, +} from '@nestjs/mongoose'; +import { + DATABASE_CONNECTION_NAME, + DATABASE_CREATED_AT_FIELD_NAME, + DATABASE_UPDATED_AT_FIELD_NAME, +} from 'src/common/database/constants/database.constant'; + +export function DatabaseConnection( + connectionName?: string +): ParameterDecorator { + return InjectConnection(connectionName ?? DATABASE_CONNECTION_NAME); +} + +export function DatabaseModel( + entity: any, + connectionName?: string +): ParameterDecorator { + return InjectModel(entity, connectionName ?? DATABASE_CONNECTION_NAME); +} + +export function DatabaseEntity(options?: SchemaOptions): ClassDecorator { + return Schema({ + ...options, + versionKey: false, + timestamps: { + createdAt: DATABASE_CREATED_AT_FIELD_NAME, + updatedAt: DATABASE_UPDATED_AT_FIELD_NAME, + }, + }); +} diff --git a/src/common/database/interfaces/database.interface.ts b/src/common/database/interfaces/database.interface.ts new file mode 100644 index 0000000..4b81bbf --- /dev/null +++ b/src/common/database/interfaces/database.interface.ts @@ -0,0 +1,54 @@ +import { PopulateOptions } from 'mongoose'; +import { IPaginationOptions } from 'src/common/pagination/interfaces/pagination.interface'; + +// find one +export interface IDatabaseFindOneOptions + extends Pick { + select?: Record; + join?: boolean | PopulateOptions | PopulateOptions[]; + session?: T; + withDeleted?: boolean; +} + +export type IDatabaseOptions = Pick< + IDatabaseFindOneOptions, + 'session' | 'withDeleted' | 'join' +>; + +// find +export interface IDatabaseFindAllOptions + extends IPaginationOptions, + Omit, 'order'> {} + +// create + +export interface IDatabaseCreateOptions + extends Pick, 'session'> { + _id?: string; +} + +// exist + +export interface IDatabaseExistOptions extends IDatabaseOptions { + excludeId?: string[]; +} + +// bulk +export type IDatabaseManyOptions = Pick< + IDatabaseFindOneOptions, + 'session' | 'join' +>; + +export type IDatabaseCreateManyOptions = Pick< + IDatabaseOptions, + 'session' +>; + +export type IDatabaseSoftDeleteManyOptions = IDatabaseManyOptions; + +export type IDatabaseRestoreManyOptions = IDatabaseManyOptions; + +export type IDatabaseRawOptions = Pick< + IDatabaseOptions, + 'session' | 'withDeleted' +>; diff --git a/src/common/database/interfaces/database.options-service.interface.ts b/src/common/database/interfaces/database.options-service.interface.ts new file mode 100644 index 0000000..e217fa3 --- /dev/null +++ b/src/common/database/interfaces/database.options-service.interface.ts @@ -0,0 +1,5 @@ +import { MongooseModuleOptions } from '@nestjs/mongoose'; + +export interface IDatabaseOptionsService { + createOptions(): MongooseModuleOptions; +} diff --git a/src/common/database/services/database.options.service.ts b/src/common/database/services/database.options.service.ts new file mode 100644 index 0000000..8765db4 --- /dev/null +++ b/src/common/database/services/database.options.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { MongooseModuleOptions } from '@nestjs/mongoose'; +import mongoose from 'mongoose'; +import { ConfigService } from '@nestjs/config'; +import { IDatabaseOptionsService } from 'src/common/database/interfaces/database.options-service.interface'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; + +@Injectable() +export class DatabaseOptionsService implements IDatabaseOptionsService { + private readonly host: string; + private readonly database: string; + private readonly user: string; + private readonly password: string; + private readonly debug: boolean; + private readonly options: string; + private readonly env: string; + + constructor(private readonly configService: ConfigService) { + this.env = this.configService.get('app.env'); + this.host = this.configService.get('database.host'); + this.database = this.configService.get('database.name'); + this.user = this.configService.get('database.user'); + this.password = this.configService.get('database.password'); + this.debug = this.configService.get('database.debug'); + + this.options = this.configService.get('database.options') + ? `?${this.configService.get('database.options')}` + : ''; + } + + createOptions(): MongooseModuleOptions { + let uri = `${this.host}`; + + if (this.database) { + uri = `${uri}/${this.database}${this.options}`; + } + + if (this.env !== ENUM_APP_ENVIRONMENT.PRODUCTION) { + mongoose.set('debug', this.debug); + } + + const mongooseOptions: MongooseModuleOptions = { + uri, + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + autoCreate: true, + // useMongoClient: true, + }; + + if (this.user && this.password) { + mongooseOptions.auth = { + username: this.user, + password: this.password, + }; + } + + return mongooseOptions; + } +} diff --git a/src/common/debugger/constants/debugger.constant.ts b/src/common/debugger/constants/debugger.constant.ts new file mode 100644 index 0000000..a5bc018 --- /dev/null +++ b/src/common/debugger/constants/debugger.constant.ts @@ -0,0 +1,5 @@ +export const DEBUGGER_NAME = 'system'; + +export const DEBUGGER_HTTP_FORMAT = + "':remote-addr' - ':remote-user' - '[:date[iso]]' - 'HTTP/:http-version' - '[:status]' - ':method' - ':url' - 'Request Header :: :req-headers' - 'Request Params :: :req-params' - 'Request Body :: :req-body' - 'Response Header :: :res[header]' - 'Response Body :: :res-body' - ':response-time ms' - ':referrer' - ':user-agent'"; +export const DEBUGGER_HTTP_NAME = 'http'; diff --git a/src/common/debugger/debugger.module.ts b/src/common/debugger/debugger.module.ts new file mode 100644 index 0000000..89e242e --- /dev/null +++ b/src/common/debugger/debugger.module.ts @@ -0,0 +1,51 @@ +import { + DynamicModule, + ForwardReference, + Global, + Module, + Provider, + Type, +} from '@nestjs/common'; +import { WinstonModule } from 'nest-winston'; +import { DebuggerOptionsModule } from 'src/common/debugger/debugger.options.module'; +import { DebuggerMiddlewareModule } from 'src/common/debugger/middleware/debugger.middleware.module'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; + +@Global() +@Module({}) +export class DebuggerModule { + static forRoot(): DynamicModule { + const providers: Provider[] = []; + const imports: ( + | DynamicModule + | Type + | Promise + | ForwardReference + )[] = []; + + if ( + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE === 'true' || + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE === 'true' + ) { + providers.push(DebuggerService); + imports.push( + WinstonModule.forRootAsync({ + inject: [DebuggerOptionService], + imports: [DebuggerOptionsModule], + useFactory: ( + debuggerOptionsService: DebuggerOptionService + ) => debuggerOptionsService.createLogger(), + }) + ); + } + + return { + module: DebuggerModule, + providers, + exports: providers, + controllers: [], + imports: [...imports, DebuggerMiddlewareModule], + }; + } +} diff --git a/src/common/debugger/debugger.options.module.ts b/src/common/debugger/debugger.options.module.ts new file mode 100644 index 0000000..6e8c5f8 --- /dev/null +++ b/src/common/debugger/debugger.options.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; + +@Module({ + providers: [DebuggerOptionService], + exports: [DebuggerOptionService], + imports: [], +}) +export class DebuggerOptionsModule {} diff --git a/src/common/debugger/interfaces/debugger.interface.ts b/src/common/debugger/interfaces/debugger.interface.ts new file mode 100644 index 0000000..bb9cfcb --- /dev/null +++ b/src/common/debugger/interfaces/debugger.interface.ts @@ -0,0 +1,22 @@ +import { RotatingFileStream } from 'rotating-file-stream'; +import { Response } from 'express'; + +export interface IDebuggerLog { + description: string; + class?: string; + function?: string; + path?: string; +} + +export interface IDebuggerHttpConfigOptions { + readonly stream: RotatingFileStream; +} + +export interface IDebuggerHttpConfig { + readonly debuggerHttpFormat: string; + readonly debuggerHttpOptions?: IDebuggerHttpConfigOptions; +} + +export interface IDebuggerHttpMiddleware extends Response { + body: string; +} diff --git a/src/common/debugger/interfaces/debugger.options-service.interface.ts b/src/common/debugger/interfaces/debugger.options-service.interface.ts new file mode 100644 index 0000000..af8bb19 --- /dev/null +++ b/src/common/debugger/interfaces/debugger.options-service.interface.ts @@ -0,0 +1,5 @@ +import { LoggerOptions } from 'winston'; + +export interface IDebuggerOptionService { + createLogger(): LoggerOptions; +} diff --git a/src/common/debugger/interfaces/debugger.service.interface.ts b/src/common/debugger/interfaces/debugger.service.interface.ts new file mode 100644 index 0000000..509ff1f --- /dev/null +++ b/src/common/debugger/interfaces/debugger.service.interface.ts @@ -0,0 +1,11 @@ +import { IDebuggerLog } from 'src/common/debugger/interfaces/debugger.interface'; + +export interface IDebuggerService { + info(requestId: string, log: IDebuggerLog, data?: any): void; + + debug(requestId: string, log: IDebuggerLog, data?: any): void; + + warn(requestId: string, log: IDebuggerLog, data?: any): void; + + error(requestId: string, log: IDebuggerLog, data?: any): void; +} diff --git a/src/common/debugger/middleware/debugger.middleware.module.ts b/src/common/debugger/middleware/debugger.middleware.module.ts new file mode 100644 index 0000000..3ec2760 --- /dev/null +++ b/src/common/debugger/middleware/debugger.middleware.module.ts @@ -0,0 +1,21 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { + DebuggerHttpMiddleware, + DebuggerHttpResponseMiddleware, + DebuggerHttpWriteIntoConsoleMiddleware, + DebuggerHttpWriteIntoFileMiddleware, +} from 'src/common/debugger/middleware/http/debugger.http.middleware'; + +@Module({}) +export class DebuggerMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply( + DebuggerHttpResponseMiddleware, + DebuggerHttpMiddleware, + DebuggerHttpWriteIntoConsoleMiddleware, + DebuggerHttpWriteIntoFileMiddleware + ) + .forRoutes('*'); + } +} diff --git a/src/common/debugger/middleware/http/debugger.http.middleware.ts b/src/common/debugger/middleware/http/debugger.http.middleware.ts new file mode 100644 index 0000000..5cbd51f --- /dev/null +++ b/src/common/debugger/middleware/http/debugger.http.middleware.ts @@ -0,0 +1,170 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import morgan from 'morgan'; +import { Request, Response, NextFunction } from 'express'; +import { createStream } from 'rotating-file-stream'; +import { ConfigService } from '@nestjs/config'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { + IDebuggerHttpConfig, + IDebuggerHttpConfigOptions, + IDebuggerHttpMiddleware, +} from 'src/common/debugger/interfaces/debugger.interface'; +import { + DEBUGGER_HTTP_FORMAT, + DEBUGGER_HTTP_NAME, +} from 'src/common/debugger/constants/debugger.constant'; + +@Injectable() +export class DebuggerHttpMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + } + + private customToken(): void { + morgan.token('req-params', (req: Request) => + JSON.stringify(req.params) + ); + + morgan.token('req-body', (req: Request) => JSON.stringify(req.body)); + + morgan.token( + 'res-body', + (req: Request, res: IDebuggerHttpMiddleware) => res.body + ); + + morgan.token('req-headers', (req: Request) => + JSON.stringify(req.headers) + ); + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoConsole || this.writeIntoFile) { + this.customToken(); + } + + next(); + } +} + +@Injectable() +export class DebuggerHttpWriteIntoFileMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly maxSize: string; + private readonly maxFiles: number; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + this.maxSize = this.configService.get('debugger.http.maxSize'); + this.maxFiles = this.configService.get( + 'debugger.http.maxFiles' + ); + } + + private async httpLogger(): Promise { + const date: string = this.helperDateService.format( + this.helperDateService.create() + ); + + const debuggerHttpOptions: IDebuggerHttpConfigOptions = { + stream: createStream(`${date}.log`, { + path: `./logs/${DEBUGGER_HTTP_NAME}/`, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + compress: true, + interval: '1d', + }), + }; + + return { + debuggerHttpFormat: DEBUGGER_HTTP_FORMAT, + debuggerHttpOptions, + }; + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoFile) { + const config: IDebuggerHttpConfig = await this.httpLogger(); + + morgan(config.debuggerHttpFormat, config.debuggerHttpOptions)( + req, + res, + next + ); + } else { + next(); + } + } +} + +@Injectable() +export class DebuggerHttpWriteIntoConsoleMiddleware implements NestMiddleware { + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + } + + private async httpLogger(): Promise { + return { + debuggerHttpFormat: DEBUGGER_HTTP_FORMAT, + }; + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoConsole) { + const config: IDebuggerHttpConfig = await this.httpLogger(); + + morgan(config.debuggerHttpFormat)(req, res, next); + } else { + next(); + } + } +} + +@Injectable() +export class DebuggerHttpResponseMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + } + use(req: Request, res: Response, next: NextFunction): void { + if (this.writeIntoConsole || this.writeIntoFile) { + const send: any = res.send; + const resOld: any = res; + + // Add response data to request + // this is for morgan + resOld.send = (body: any) => { + resOld.body = body; + resOld.send = send; + resOld.send(body); + + res = resOld as Response; + }; + } + + next(); + } +} diff --git a/src/common/debugger/services/debugger.options.service.ts b/src/common/debugger/services/debugger.options.service.ts new file mode 100644 index 0000000..7bcc794 --- /dev/null +++ b/src/common/debugger/services/debugger.options.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { LoggerOptions } from 'winston'; +import winston from 'winston'; +import DailyRotateFile from 'winston-daily-rotate-file'; +import { DEBUGGER_NAME } from 'src/common/debugger/constants/debugger.constant'; +import { IDebuggerOptionService } from 'src/common/debugger/interfaces/debugger.options-service.interface'; + +@Injectable() +export class DebuggerOptionService implements IDebuggerOptionService { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + private readonly maxSize: string; + private readonly maxFiles: string; + + constructor(private configService: ConfigService) { + this.writeIntoFile = this.configService.get( + 'debugger.system.writeIntoFile' + ); + this.writeIntoConsole = this.configService.get( + 'debugger.system.writeIntoConsole' + ); + this.maxSize = this.configService.get( + 'debugger.system.maxSize' + ); + this.maxFiles = this.configService.get( + 'debugger.system.maxFiles' + ); + } + + createLogger(): LoggerOptions { + const transports = []; + + if (this.writeIntoFile) { + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/error`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'error', + }) + ); + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/default`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'info', + }) + ); + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/debug`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'debug', + }) + ); + } + + if (this.writeIntoConsole) { + transports.push(new winston.transports.Console()); + } + + const loggerOptions: LoggerOptions = { + format: winston.format.combine( + winston.format.timestamp(), + winston.format.prettyPrint() + ), + transports, + }; + + return loggerOptions; + } +} diff --git a/src/common/debugger/services/debugger.service.ts b/src/common/debugger/services/debugger.service.ts new file mode 100644 index 0000000..23b9a7d --- /dev/null +++ b/src/common/debugger/services/debugger.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Logger } from 'winston'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { IDebuggerLog } from 'src/common/debugger/interfaces/debugger.interface'; +import { IDebuggerService } from 'src/common/debugger/interfaces/debugger.service.interface'; + +@Injectable() +export class DebuggerService implements IDebuggerService { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) + private readonly logger: Logger + ) {} + + info(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.info(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + debug(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.debug(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + warn(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.warn(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + error(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.error(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } +} diff --git a/src/common/doc/constants/doc.enum.constant.ts b/src/common/doc/constants/doc.enum.constant.ts new file mode 100644 index 0000000..0739cf1 --- /dev/null +++ b/src/common/doc/constants/doc.enum.constant.ts @@ -0,0 +1,11 @@ +export enum ENUM_DOC_REQUEST_BODY_TYPE { + JSON = 'JSON', + FORM_DATA = 'FORM_DATA', + TEXT = 'TEXT', +} + +export enum ENUM_DOC_RESPONSE_BODY_TYPE { + JSON = 'JSON', + FILE = 'FILE', + TEXT = 'TEXT', +} diff --git a/src/common/doc/decorators/doc.decorator.ts b/src/common/doc/decorators/doc.decorator.ts new file mode 100644 index 0000000..79039e2 --- /dev/null +++ b/src/common/doc/decorators/doc.decorator.ts @@ -0,0 +1,690 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiHeader, + ApiHeaders, + ApiParam, + ApiProduces, + ApiQuery, + ApiResponse, + ApiSecurity, + getSchemaPath, +} from '@nestjs/swagger'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; +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_DOC_REQUEST_BODY_TYPE, + ENUM_DOC_RESPONSE_BODY_TYPE, +} from 'src/common/doc/constants/doc.enum.constant'; +import { + IDocDefaultOptions, + IDocOfOptions, + IDocOptions, + IDocPagingOptions, +} from 'src/common/doc/interfaces/doc.interface'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { FileMultipleDto } from 'src/common/file/dtos/file.multiple.dto'; +import { FileSingleDto } from 'src/common/file/dtos/file.single.dto'; +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 { Skip } from 'src/common/request/validations/request.skip.validation'; +import { ResponseDefaultSerialization } from 'src/common/response/serializations/response.default.serialization'; +import { ResponsePagingSerialization } from 'src/common/response/serializations/response.paging.serialization'; + +export function Doc( + messagePath: string, + options?: IDocOptions +): MethodDecorator { + const docs = []; + const normDoc: IDocDefaultOptions = { + httpStatus: options?.response?.httpStatus ?? HttpStatus.OK, + messagePath, + statusCode: options?.response?.statusCode, + }; + + if (!normDoc.statusCode) { + normDoc.statusCode = normDoc.httpStatus; + } + + if (options?.request?.bodyType === ENUM_DOC_REQUEST_BODY_TYPE.FORM_DATA) { + docs.push(ApiConsumes('multipart/form-data')); + + if (options?.request?.file?.multiple) { + docs.push( + ApiBody({ + description: 'Multiple file', + type: FileMultipleDto, + }) + ); + } else if (!options?.request?.file?.multiple) { + docs.push( + ApiBody({ + description: 'Single file', + type: FileSingleDto, + }) + ); + } + } else if (options?.request?.bodyType === ENUM_DOC_REQUEST_BODY_TYPE.TEXT) { + docs.push(ApiConsumes('text/plain')); + } else { + docs.push(ApiConsumes('application/json')); + } + + if (options?.response?.bodyType === ENUM_DOC_RESPONSE_BODY_TYPE.FILE) { + docs.push(ApiProduces(ENUM_FILE_EXCEL_MIME.XLSX)); + } else if ( + options?.response?.bodyType === ENUM_DOC_RESPONSE_BODY_TYPE.TEXT + ) { + docs.push(ApiProduces('text/plain')); + } else { + docs.push(ApiProduces('application/json')); + if (options?.response?.serialization) { + normDoc.serialization = options?.response?.serialization; + } + } + docs.push(DocDefault(normDoc)); + + if (options?.request?.params) { + docs.push(...options?.request?.params.map((param) => ApiParam(param))); + } + + if (options?.request?.queries) { + docs.push(...options?.request?.queries.map((query) => ApiQuery(query))); + } + + const oneOfUnauthorized: IDocOfOptions[] = []; + const oneOfForbidden: IDocOfOptions[] = []; + + // auth + const auths = []; + if (options?.auth?.jwtRefreshToken) { + auths.push(ApiBearerAuth('refreshToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.refreshTokenUnauthorized', + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR, + }); + } + + if (options?.auth?.jwtAccessToken) { + auths.push(ApiBearerAuth('accessToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.accessTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + }); + oneOfForbidden.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + messagePath: 'auth.error.permissionForbidden', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + messagePath: 'auth.error.accessForForbidden', + } + ); + } + + if (options?.auth?.apiKey) { + auths.push(ApiSecurity('apiKey')); + oneOfUnauthorized.push( + { + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + messagePath: 'apiKey.error.keyNeeded', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + messagePath: 'apiKey.error.notFound', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INACTIVE_ERROR, + messagePath: 'apiKey.error.inactive', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + messagePath: 'apiKey.error.invalid', + } + ); + } + + if (options?.auth?.permissionToken) { + auths.push(ApiSecurity('permissionToken')); + oneOfUnauthorized.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + messagePath: 'auth.error.permissionTokenUnauthorized', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + messagePath: 'auth.error.permissionTokenInvalid', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + messagePath: 'auth.error.permissionTokenNotYour', + } + ); + } + + // request headers + const requestHeaders = []; + if (options?.requestHeader?.userAgent) { + oneOfForbidden.push( + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_INVALID_ERROR, + messagePath: 'request.error.userAgentInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + messagePath: 'request.error.userAgentBrowserInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + messagePath: 'request.error.userAgentOsInvalid', + } + ); + requestHeaders.push({ + name: 'user-agent', + description: 'User agent header', + required: true, + schema: { + example: + 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion', + type: 'string', + }, + }); + } + + if (options?.requestHeader?.timestamp) { + oneOfForbidden.push({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + messagePath: 'request.error.timestampInvalid', + }); + requestHeaders.push({ + name: 'x-timestamp', + description: 'Timestamp header, in microseconds', + required: true, + schema: { + example: 1662876305642, + type: 'number', + }, + }); + } + + return applyDecorators( + ApiHeader({ + name: 'x-custom-lang', + description: 'Custom language header', + required: false, + schema: { + default: APP_LANGUAGE, + example: APP_LANGUAGE, + type: 'string', + }, + }), + ApiHeaders(requestHeaders), + DocDefault({ + httpStatus: HttpStatus.SERVICE_UNAVAILABLE, + messagePath: 'http.serverError.serviceUnavailable', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + }), + DocDefault({ + httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, + messagePath: 'http.serverError.internalServerError', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + }), + DocDefault({ + httpStatus: HttpStatus.REQUEST_TIMEOUT, + messagePath: 'http.serverError.requestTimeout', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + }), + oneOfForbidden.length > 0 + ? DocOneOf(HttpStatus.FORBIDDEN, ...oneOfForbidden) + : Skip(), + oneOfUnauthorized.length > 0 + ? DocOneOf(HttpStatus.UNAUTHORIZED, ...oneOfUnauthorized) + : Skip(), + ...auths, + ...docs + ); +} + +export function DocPaging( + messagePath: string, + options: IDocPagingOptions +): MethodDecorator { + // paging + const docs = []; + + if (options?.request?.params) { + docs.push(...options?.request?.params.map((param) => ApiParam(param))); + } + + if (options?.request?.queries) { + docs.push(...options?.request?.queries.map((query) => ApiQuery(query))); + } + + const oneOfUnauthorized: IDocOfOptions[] = []; + const oneOfForbidden: IDocOfOptions[] = []; + + // auth + const auths = []; + if (options?.auth?.jwtRefreshToken) { + auths.push(ApiBearerAuth('refreshToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.refreshTokenUnauthorized', + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR, + }); + } + + if (options?.auth?.jwtAccessToken) { + auths.push(ApiBearerAuth('accessToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.accessTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + }); + oneOfForbidden.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + messagePath: 'auth.error.permissionForbidden', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + messagePath: 'auth.error.accessForForbidden', + } + ); + } + + if (options?.auth?.apiKey) { + auths.push(ApiSecurity('apiKey')); + oneOfUnauthorized.push( + { + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + messagePath: 'apiKey.error.keyNeeded', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + messagePath: 'apiKey.error.notFound', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INACTIVE_ERROR, + messagePath: 'apiKey.error.inactive', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + messagePath: 'apiKey.error.invalid', + } + ); + } + + if (options?.auth?.permissionToken) { + auths.push(ApiSecurity('permissionToken')); + oneOfUnauthorized.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + messagePath: 'auth.error.permissionTokenUnauthorized', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + messagePath: 'auth.error.permissionTokenInvalid', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + messagePath: 'auth.error.permissionTokenNotYour', + } + ); + } + + // request headers + const requestHeaders = []; + if (options?.requestHeader?.userAgent) { + oneOfForbidden.push( + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_INVALID_ERROR, + messagePath: 'request.error.userAgentInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + messagePath: 'request.error.userAgentBrowserInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + messagePath: 'request.error.userAgentOsInvalid', + } + ); + requestHeaders.push({ + name: 'user-agent', + description: 'User agent header', + required: true, + schema: { + example: + 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion', + type: 'string', + }, + }); + } + + if (options?.requestHeader?.timestamp) { + oneOfForbidden.push({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + messagePath: 'request.error.timestampInvalid', + }); + requestHeaders.push({ + name: 'x-timestamp', + description: 'Timestamp header, in microseconds', + required: true, + schema: { + example: 1662876305642, + type: 'number', + }, + }); + } + + return applyDecorators( + // paging + ApiConsumes('application/json'), + ApiExtraModels(ResponsePagingSerialization), + ApiExtraModels(options.response.serialization), + ApiResponse({ + status: HttpStatus.OK, + schema: { + allOf: [ + { $ref: getSchemaPath(ResponsePagingSerialization) }, + ], + properties: { + message: { + example: messagePath, + }, + statusCode: { + type: 'number', + example: options.response.statusCode ?? HttpStatus.OK, + }, + data: { + type: 'array', + items: { + $ref: getSchemaPath(options.response.serialization), + }, + }, + }, + }, + }), + ApiQuery({ + name: 'search', + required: false, + allowEmptyValue: true, + type: 'string', + description: + 'Search will base on _availableSearch with rule contains, and case insensitive', + }), + ApiQuery({ + name: 'perPage', + required: false, + allowEmptyValue: true, + example: 20, + type: 'number', + description: 'Data per page', + }), + ApiQuery({ + name: 'page', + required: false, + allowEmptyValue: true, + example: 1, + type: 'number', + description: 'page number', + }), + ApiQuery({ + name: 'orderBy', + required: false, + allowEmptyValue: true, + example: 'createdAt', + type: 'string', + description: 'Order by base on _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 _availableOrderDirection', + }), + + // default + ApiHeader({ + name: 'x-custom-lang', + description: 'Custom language header', + required: false, + schema: { + default: APP_LANGUAGE, + example: APP_LANGUAGE, + type: 'string', + }, + }), + ApiHeaders(requestHeaders), + DocDefault({ + httpStatus: HttpStatus.SERVICE_UNAVAILABLE, + messagePath: 'http.serverError.serviceUnavailable', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + }), + DocDefault({ + httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, + messagePath: 'http.serverError.internalServerError', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + }), + DocDefault({ + httpStatus: HttpStatus.REQUEST_TIMEOUT, + messagePath: 'http.serverError.requestTimeout', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + }), + oneOfForbidden.length > 0 + ? DocOneOf(HttpStatus.FORBIDDEN, ...oneOfForbidden) + : Skip(), + oneOfUnauthorized.length > 0 + ? DocOneOf(HttpStatus.UNAUTHORIZED, ...oneOfUnauthorized) + : Skip(), + ...auths, + ...docs + ); +} + +export function DocDefault(options: IDocDefaultOptions): MethodDecorator { + const docs = []; + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: options.messagePath, + }, + statusCode: { + type: 'number', + example: options.statusCode, + }, + }, + }; + + if (options.serialization) { + docs.push(ApiExtraModels(options.serialization)); + schema.properties = { + ...schema.properties, + data: { + $ref: getSchemaPath(options.serialization), + }, + }; + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: options.httpStatus, + schema, + }), + ...docs + ); +} + +export function DocOneOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const oneOf = []; + + for (const doc of documents) { + const oneOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + oneOfSchema.properties = { + ...oneOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + oneOf.push(oneOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + oneOf, + }, + }), + ...docs + ); +} + +export function DocAnyOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const anyOf = []; + + for (const doc of documents) { + const anyOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + anyOfSchema.properties = { + ...anyOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + anyOf.push(anyOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + anyOf, + }, + }), + ...docs + ); +} + +export function DocAllOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const allOf = []; + + for (const doc of documents) { + const allOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + allOfSchema.properties = { + ...allOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + allOf.push(allOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + allOf, + }, + }), + ...docs + ); +} diff --git a/src/common/doc/interfaces/doc.interface.ts b/src/common/doc/interfaces/doc.interface.ts new file mode 100644 index 0000000..558a0ea --- /dev/null +++ b/src/common/doc/interfaces/doc.interface.ts @@ -0,0 +1,66 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger'; +import { ClassConstructor } from 'class-transformer'; +import { + ENUM_DOC_REQUEST_BODY_TYPE, + ENUM_DOC_RESPONSE_BODY_TYPE, +} from 'src/common/doc/constants/doc.enum.constant'; + +export interface IDocOfOptions { + messagePath: string; + statusCode: number; + serialization?: ClassConstructor; +} + +export interface IDocDefaultOptions { + httpStatus: HttpStatus; + messagePath: string; + statusCode: number; + serialization?: ClassConstructor; +} + +export interface IDocOptions { + auth?: IDocAuthOptions; + requestHeader?: IDocRequestHeaderOptions; + response?: IDocResponseOptions; + request?: IDocRequestOptions; +} + +export interface IDocPagingOptions + extends Omit, 'response' | 'request'> { + response: IDocPagingResponseOptions; + request?: Omit; +} + +export interface IDocResponseOptions { + statusCode?: number; + httpStatus?: HttpStatus; + bodyType?: ENUM_DOC_RESPONSE_BODY_TYPE; + serialization?: ClassConstructor; +} + +export interface IDocPagingResponseOptions + extends Pick, 'statusCode'> { + serialization: ClassConstructor; +} + +export interface IDocAuthOptions { + jwtAccessToken?: boolean; + jwtRefreshToken?: boolean; + apiKey?: boolean; + permissionToken?: boolean; +} + +export interface IDocRequestHeaderOptions { + userAgent?: boolean; + timestamp?: boolean; +} + +export interface IDocRequestOptions { + params?: ApiParamOptions[]; + queries?: ApiQueryOptions[]; + bodyType?: ENUM_DOC_REQUEST_BODY_TYPE; + file?: { + multiple: boolean; + }; +} diff --git a/src/common/error/constants/error.constant.ts b/src/common/error/constants/error.constant.ts new file mode 100644 index 0000000..64a68c3 --- /dev/null +++ b/src/common/error/constants/error.constant.ts @@ -0,0 +1,2 @@ +export const ERROR_CLASS_META_KEY = 'ErrorMetaClassKey'; +export const ERROR_FUNCTION_META_KEY = 'ErrorMetaFunctionKey'; diff --git a/src/common/error/constants/error.enum.constant.ts b/src/common/error/constants/error.enum.constant.ts new file mode 100644 index 0000000..3caccc0 --- /dev/null +++ b/src/common/error/constants/error.enum.constant.ts @@ -0,0 +1,4 @@ +export enum ERROR_TYPE { + DEFAULT = 'DEFAULT', + IMPORT = 'IMPORT', +} diff --git a/src/common/error/constants/error.status-code.constant.ts b/src/common/error/constants/error.status-code.constant.ts new file mode 100644 index 0000000..ccb99bd --- /dev/null +++ b/src/common/error/constants/error.status-code.constant.ts @@ -0,0 +1,5 @@ +export enum ENUM_ERROR_STATUS_CODE_ERROR { + ERROR_UNKNOWN = 5990, + ERROR_SERVICE_UNAVAILABLE = 5991, + ERROR_REQUEST_TIMEOUT = 5992, +} diff --git a/src/common/error/decorators/error.decorator.ts b/src/common/error/decorators/error.decorator.ts new file mode 100644 index 0000000..7b14acb --- /dev/null +++ b/src/common/error/decorators/error.decorator.ts @@ -0,0 +1,12 @@ +import { applyDecorators, SetMetadata } from '@nestjs/common'; +import { + ERROR_CLASS_META_KEY, + ERROR_FUNCTION_META_KEY, +} from 'src/common/error/constants/error.constant'; + +export function ErrorMeta(cls: string, func: string): MethodDecorator { + return applyDecorators( + SetMetadata(ERROR_CLASS_META_KEY, cls), + SetMetadata(ERROR_FUNCTION_META_KEY, func) + ); +} diff --git a/src/common/error/error.module.ts b/src/common/error/error.module.ts new file mode 100644 index 0000000..b514375 --- /dev/null +++ b/src/common/error/error.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD } from '@nestjs/core'; +import { ErrorHttpFilter } from './filters/error.http.filter'; +import { ErrorMetaGuard } from './guards/error.meta.guard'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_FILTER, + useClass: ErrorHttpFilter, + }, + { + provide: APP_GUARD, + useClass: ErrorMetaGuard, + }, + ], + imports: [], +}) +export class ErrorModule {} diff --git a/src/common/error/filters/error.http.filter.ts b/src/common/error/filters/error.http.filter.ts new file mode 100644 index 0000000..61fabc7 --- /dev/null +++ b/src/common/error/filters/error.http.filter.ts @@ -0,0 +1,186 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Optional, +} from '@nestjs/common'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { ConfigService } from '@nestjs/config'; +import { ValidationError } from 'class-validator'; +import { Response } from 'express'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; +import { ERROR_TYPE } from 'src/common/error/constants/error.enum.constant'; +import { + IErrorException, + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { ErrorMetadataSerialization } from 'src/common/error/serializations/error.serialization'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { + IMessage, + 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'; + +// If we throw error with HttpException, there will always return object +// The exception filter only catch HttpException +@Catch() +export class ErrorHttpFilter implements ExceptionFilter { + private readonly appDefaultLanguage: string[]; + + constructor( + @Optional() private readonly debuggerService: DebuggerService, + private readonly configService: ConfigService, + private readonly messageService: MessageService, + private readonly helperDateService: HelperDateService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async catch(exception: unknown, host: ArgumentsHost): Promise { + const ctx: HttpArgumentsHost = host.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + // get request headers + const __customLang: string[] = + request.__customLang ?? this.appDefaultLanguage; + const __class = request.__class ?? ErrorHttpFilter.name; + const __function = request.__function ?? this.catch.name; + const __requestId = request.__id ?? DatabaseDefaultUUID(); + const __path = request.path; + const __timestamp = + request.__xTimestamp ?? + request.__timestamp ?? + this.helperDateService.timestamp(); + const __timezone = + request.__timezone ?? + Intl.DateTimeFormat().resolvedOptions().timeZone; + const __version = + request.__version ?? + this.configService.get('app.versioning.version'); + const __repoVersion = + request.__repoVersion ?? + this.configService.get('app.repoVersion'); + + // Debugger + try { + this.debuggerService.error( + request?.__id ? request.__id : ErrorHttpFilter.name, + { + description: + exception instanceof Error + ? exception.message + : exception.toString(), + class: __class ?? ErrorHttpFilter.name, + function: __function ?? this.catch.name, + path: __path, + }, + exception + ); + } catch (err: unknown) {} + + // set default + let statusHttp: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + let messagePath = `http.${statusHttp}`; + let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + let _error: string = undefined; + let errors: IErrors[] | IErrorsImport[] = undefined; + let messageProperties: IMessageOptionsProperties = undefined; + let data: Record = undefined; + let metadata: ErrorMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + if (exception instanceof HttpException) { + // Restructure + const responseException = exception.getResponse(); + statusHttp = exception.getStatus(); + messagePath = `http.${statusHttp}`; + statusCode = exception.getStatus(); + + if (this.isErrorException(responseException)) { + const { _metadata } = responseException; + + statusCode = responseException.statusCode; + messagePath = responseException.message; + data = responseException.data; + messageProperties = + _metadata?.customProperty?.messageProperties; + delete _metadata?.customProperty; + + metadata = { + ...metadata, + ..._metadata, + }; + + if (responseException.errors?.length > 0) { + errors = + responseException._errorType === ERROR_TYPE.IMPORT + ? await this.messageService.getImportErrorsMessage( + responseException.errors as IValidationErrorImport[], + __customLang + ) + : await this.messageService.getRequestErrorsMessage( + responseException.errors as ValidationError[], + __customLang + ); + } + + if (!responseException._error) { + _error = + typeof responseException._error !== 'string' + ? JSON.stringify(responseException._error) + : responseException._error; + } + } + } + + const message: string | IMessage = await this.messageService.get( + messagePath, + { + customLanguages: __customLang, + properties: messageProperties, + } + ); + + const responseBody = { + statusCode, + message, + errors, + _error, + _metadata: metadata, + data, + }; + + response + .setHeader('x-custom-lang', __customLang) + .setHeader('x-timestamp', __timestamp) + .setHeader('x-timezone', __timezone) + .setHeader('x-request-id', __requestId) + .setHeader('x-version', __version) + .setHeader('x-repo-version', __repoVersion) + .status(statusHttp) + .json(responseBody); + + return; + } + + isErrorException(obj: any): obj is IErrorException { + return typeof obj === 'object' + ? 'statusCode' in obj && 'message' in obj + : false; + } +} diff --git a/src/common/error/guards/error.meta.guard.ts b/src/common/error/guards/error.meta.guard.ts new file mode 100644 index 0000000..73c277c --- /dev/null +++ b/src/common/error/guards/error.meta.guard.ts @@ -0,0 +1,31 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { + ERROR_CLASS_META_KEY, + ERROR_FUNCTION_META_KEY, +} from 'src/common/error/constants/error.constant'; + +@Injectable() +export class ErrorMetaGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const cls = this.reflector.get( + ERROR_CLASS_META_KEY, + context.getHandler() + ); + const func = this.reflector.get( + ERROR_FUNCTION_META_KEY, + context.getHandler() + ); + + const className = context.getClass().name; + const methodKey = context.getHandler().name; + + request.__class = cls ?? className; + request.__function = func ?? methodKey; + + return true; + } +} diff --git a/src/common/error/interfaces/error.interface.ts b/src/common/error/interfaces/error.interface.ts new file mode 100644 index 0000000..c77afa9 --- /dev/null +++ b/src/common/error/interfaces/error.interface.ts @@ -0,0 +1,48 @@ +import { ValidationError } from 'class-validator'; +import { ERROR_TYPE } from 'src/common/error/constants/error.enum.constant'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; +import { IResponseCustomPropertyMetadata } from 'src/common/response/interfaces/response.interface'; + +// error default +export interface IErrors { + readonly message: string | IMessage; + readonly property: string; +} + +// error import +export interface IErrorsImport { + row: number; + file?: string; + errors: IErrors[]; +} + +export interface IValidationErrorImport extends Omit { + errors: ValidationError[]; +} + +// error exception + +export type IErrorCustomPropertyMetadata = Pick< + IResponseCustomPropertyMetadata, + 'messageProperties' +>; + +export interface IErrorMetadata { + customProperty?: IErrorCustomPropertyMetadata; + [key: string]: any; +} + +export interface IErrorException { + statusCode: number; + message: string; + errors?: ValidationError[] | IValidationErrorImport[]; + data?: Record; + _error?: string; + _errorType?: ERROR_TYPE; + _metadata?: IErrorMetadata; +} + +export interface IErrorHttpFilter + extends Omit { + message: string | IMessage; +} diff --git a/src/common/error/serializations/error.serialization.ts b/src/common/error/serializations/error.serialization.ts new file mode 100644 index 0000000..fea3c03 --- /dev/null +++ b/src/common/error/serializations/error.serialization.ts @@ -0,0 +1,3 @@ +import { ResponseMetadataSerialization } from 'src/common/response/serializations/response.default.serialization'; + +export class ErrorMetadataSerialization extends ResponseMetadataSerialization {} diff --git a/src/common/file/constants/file.constant.ts b/src/common/file/constants/file.constant.ts new file mode 100644 index 0000000..e71ba76 --- /dev/null +++ b/src/common/file/constants/file.constant.ts @@ -0,0 +1,2 @@ +export const FILE_CUSTOM_MAX_SIZE_META_KEY = 'FileCustomMaxSizeMetaKey'; +export const FILE_CUSTOM_MAX_FILES_META_KEY = 'FileCustomMaxFilesMetaKey'; diff --git a/src/common/file/constants/file.enum.constant.ts b/src/common/file/constants/file.enum.constant.ts new file mode 100644 index 0000000..99ff034 --- /dev/null +++ b/src/common/file/constants/file.enum.constant.ts @@ -0,0 +1,29 @@ +export enum ENUM_FILE_IMAGE_MIME { + JPG = 'image/jpg', + JPEG = 'image/jpeg', + PNG = 'image/png', +} + +export enum ENUM_FILE_EXCEL_MIME { + XLS = 'application/vnd.ms-excel', + XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + CSV = 'text/csv', +} + +export enum ENUM_FILE_AUDIO_MIME { + MPEG = 'audio/mpeg', + MP3 = 'audio/mp3', + MP4 = 'audio/mp4', +} + +export enum ENUM_FILE_VIDEO_MIME { + MP4 = 'video/mp4', + APPLICATION_MP4 = 'application/mp4', +} + +export enum ENUM_FILE_TYPE { + AUDIO = 'audio', + IMAGE = 'image', + EXCEL = 'excel', + VIDEO = 'video', +} diff --git a/src/common/file/constants/file.status-code.constant.ts b/src/common/file/constants/file.status-code.constant.ts new file mode 100644 index 0000000..cacda03 --- /dev/null +++ b/src/common/file/constants/file.status-code.constant.ts @@ -0,0 +1,8 @@ +export enum ENUM_FILE_STATUS_CODE_ERROR { + FILE_NEEDED_ERROR = 5950, + FILE_MAX_SIZE_ERROR = 5951, + FILE_EXTENSION_ERROR = 5952, + FILE_MAX_FILES_ERROR = 5953, + FILE_VALIDATION_DTO_ERROR = 5954, + FILE_NEED_EXTRACT_FIRST_ERROR = 5955, +} diff --git a/src/common/file/decorators/file.decorator.ts b/src/common/file/decorators/file.decorator.ts new file mode 100644 index 0000000..225ec96 --- /dev/null +++ b/src/common/file/decorators/file.decorator.ts @@ -0,0 +1,45 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; +import { + FILE_CUSTOM_MAX_FILES_META_KEY, + FILE_CUSTOM_MAX_SIZE_META_KEY, +} from 'src/common/file/constants/file.constant'; +import { FileCustomMaxFilesInterceptor } from 'src/common/file/interceptors/file.custom-max-files.interceptor'; +import { FileCustomMaxSizeInterceptor } from 'src/common/file/interceptors/file.custom-max-size.interceptor'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function UploadFileSingle(field: string): MethodDecorator { + return applyDecorators(UseInterceptors(FileInterceptor(field))); +} + +export function UploadFileMultiple(field: string): MethodDecorator { + return applyDecorators(UseInterceptors(FilesInterceptor(field))); +} + +export function FileCustomMaxFile(customMaxFiles: number): MethodDecorator { + return applyDecorators( + UseInterceptors(FileCustomMaxFilesInterceptor), + SetMetadata(FILE_CUSTOM_MAX_FILES_META_KEY, customMaxFiles) + ); +} + +export function FileCustomMaxSize(customMaxSize: string): MethodDecorator { + return applyDecorators( + UseInterceptors(FileCustomMaxSizeInterceptor), + SetMetadata(FILE_CUSTOM_MAX_SIZE_META_KEY, customMaxSize) + ); +} + +export const FilePartNumber: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const request = ctx.switchToHttp().getRequest() as IRequestApp; + const { headers } = request; + return headers['x-part-number'] ? Number(headers['x-part-number']) : 0; + } +); diff --git a/src/common/file/dtos/file.multiple.dto.ts b/src/common/file/dtos/file.multiple.dto.ts new file mode 100644 index 0000000..080b08d --- /dev/null +++ b/src/common/file/dtos/file.multiple.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FileMultipleDto { + @ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } }) + files: any[]; +} diff --git a/src/common/file/dtos/file.single.dto.ts b/src/common/file/dtos/file.single.dto.ts new file mode 100644 index 0000000..d3089a8 --- /dev/null +++ b/src/common/file/dtos/file.single.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FileSingleDto { + @ApiProperty({ type: 'string', format: 'binary' }) + file: any; +} diff --git a/src/common/file/interceptors/file.custom-max-files.interceptor.ts b/src/common/file/interceptors/file.custom-max-files.interceptor.ts new file mode 100644 index 0000000..7b9af8a --- /dev/null +++ b/src/common/file/interceptors/file.custom-max-files.interceptor.ts @@ -0,0 +1,35 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { FILE_CUSTOM_MAX_FILES_META_KEY } from 'src/common/file/constants/file.constant'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class FileCustomMaxFilesInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const request = ctx.getRequest(); + + const maxFiles: number = this.reflector.get( + FILE_CUSTOM_MAX_FILES_META_KEY, + context.getHandler() + ); + request.__customMaxFiles = maxFiles; + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/file/interceptors/file.custom-max-size.interceptor.ts b/src/common/file/interceptors/file.custom-max-size.interceptor.ts new file mode 100644 index 0000000..96403cc --- /dev/null +++ b/src/common/file/interceptors/file.custom-max-size.interceptor.ts @@ -0,0 +1,35 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { FILE_CUSTOM_MAX_SIZE_META_KEY } from 'src/common/file/constants/file.constant'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class FileCustomMaxSizeInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const request = ctx.getRequest(); + + const customSize: string = this.reflector.get( + FILE_CUSTOM_MAX_SIZE_META_KEY, + context.getHandler() + ); + request.__customMaxFileSize = customSize; + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/file/interfaces/file.interface.ts b/src/common/file/interfaces/file.interface.ts new file mode 100644 index 0000000..aab5df9 --- /dev/null +++ b/src/common/file/interfaces/file.interface.ts @@ -0,0 +1,6 @@ +export type IFile = Express.Multer.File; + +export type IFileExtract> = IFile & { + extract: Record[]; + dto?: T[]; +}; diff --git a/src/common/file/pipes/file.extract.pipe.ts b/src/common/file/pipes/file.extract.pipe.ts new file mode 100644 index 0000000..b84d153 --- /dev/null +++ b/src/common/file/pipes/file.extract.pipe.ts @@ -0,0 +1,62 @@ +import { Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { PipeTransform } from '@nestjs/common/interfaces'; +import { ENUM_FILE_EXCEL_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 { IFile, IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; + +// only for excel +@Injectable() +export class FileExtractPipe implements PipeTransform { + constructor(private readonly helperFileService: HelperFileService) {} + + async transform( + value: IFile | IFile[] + ): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + const extracts: IFileExtract[] = []; + + for (const val of value) { + await this.validate(val.mimetype); + + const extract: IFileExtract = await this.extract(val); + extracts.push(extract); + } + + return extracts; + } + + const file: IFile = value as IFile; + await this.validate(file.mimetype); + + return this.extract(file); + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + } + + async extract(value: IFile): Promise { + const extract = this.helperFileService.readExcelFromBuffer( + value.buffer + ); + + return { + ...value, + extract, + }; + } +} diff --git a/src/common/file/pipes/file.max-files.pipe.ts b/src/common/file/pipes/file.max-files.pipe.ts new file mode 100644 index 0000000..9bda249 --- /dev/null +++ b/src/common/file/pipes/file.max-files.pipe.ts @@ -0,0 +1,145 @@ +import { + PipeTransform, + Injectable, + UnprocessableEntityException, + Scope, + Inject, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { ENUM_FILE_STATUS_CODE_ERROR } from '../constants/file.status-code.constant'; + +// only for multiple upload + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesImagePipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.image.maxFiles'); + } + + async transform(value: IFile[]): Promise { + if (!value) { + return value; + } + + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesExcelPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.excel.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesVideoPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.video.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesAudioPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.audio.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.required.pipe.ts b/src/common/file/pipes/file.required.pipe.ts new file mode 100644 index 0000000..b2f3948 --- /dev/null +++ b/src/common/file/pipes/file.required.pipe.ts @@ -0,0 +1,27 @@ +import { + PipeTransform, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { IFile } from 'src/common/file/interfaces/file.interface'; + +@Injectable() +export class FileRequiredPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile | IFile[]): Promise { + if (!value || (Array.isArray(value) && value.length === 0)) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_NEEDED_ERROR, + message: 'file.error.notFound', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.size.pipe.ts b/src/common/file/pipes/file.size.pipe.ts new file mode 100644 index 0000000..a671569 --- /dev/null +++ b/src/common/file/pipes/file.size.pipe.ts @@ -0,0 +1,200 @@ +import { + PipeTransform, + Injectable, + PayloadTooLargeException, + Scope, + Inject, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import { ENUM_FILE_STATUS_CODE_ERROR } from '../constants/file.status-code.constant'; + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeImagePipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.image.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeExcelPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.excel.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeVideoPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.video.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeAudioPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.audio.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.type.pipe.ts b/src/common/file/pipes/file.type.pipe.ts new file mode 100644 index 0000000..8067a55 --- /dev/null +++ b/src/common/file/pipes/file.type.pipe.ts @@ -0,0 +1,149 @@ +import { + PipeTransform, + Injectable, + UnsupportedMediaTypeException, +} from '@nestjs/common'; +import { + ENUM_FILE_AUDIO_MIME, + ENUM_FILE_EXCEL_MIME, + ENUM_FILE_IMAGE_MIME, + ENUM_FILE_VIDEO_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 { IFile } from 'src/common/file/interfaces/file.interface'; + +@Injectable() +export class FileTypeImagePipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_IMAGE_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeVideoPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_VIDEO_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeAudioPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_AUDIO_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeExcelPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.validation.pipe.ts b/src/common/file/pipes/file.validation.pipe.ts new file mode 100644 index 0000000..a9f7994 --- /dev/null +++ b/src/common/file/pipes/file.validation.pipe.ts @@ -0,0 +1,128 @@ +import { + Injectable, + UnprocessableEntityException, + UnsupportedMediaTypeException, +} from '@nestjs/common'; +import { PipeTransform } from '@nestjs/common/interfaces'; +import { validate, ValidationError } from 'class-validator'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; +import { IValidationErrorImport } from 'src/common/error/interfaces/error.interface'; +import { IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; + +// only for excel +// must use after FileExtractPipe +@Injectable() +export class FileValidationPipe implements PipeTransform { + constructor(private readonly dto: ClassConstructor) {} + + async transform( + value: IFileExtract | IFileExtract[] + ): Promise | IFileExtract[]> { + if (!value) { + return; + } + + if (Array.isArray(value)) { + const classTransforms: IFileExtract[] = []; + for (const val of value) { + await this.validate(val); + + const classTransform: T[] = await this.transformExtract( + this.dto, + val.extract + ); + + await this.validateExtract(classTransform, val.filename); + + const classTransformMerge: IFileExtract = + await this.transformMerge(val, classTransform); + classTransforms.push(classTransformMerge); + } + + return classTransforms; + } + + const file: IFileExtract = value as IFileExtract; + await this.validate(file); + + const classTransform: T[] = await this.transformExtract( + this.dto, + file.extract + ); + + await this.validateExtract(classTransform, file.filename); + + return this.transformMerge(value, classTransform); + } + + async transformMerge( + value: IFileExtract, + classTransform: T[] + ): Promise> { + return { + ...value, + dto: classTransform, + }; + } + + async transformExtract( + classDtos: ClassConstructor, + extract: Record[] + ): Promise { + return plainToInstance(classDtos, extract); + } + + async validate(value: IFileExtract): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === value.mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } else if (!value.extract) { + throw new UnprocessableEntityException({ + statusCode: + ENUM_FILE_STATUS_CODE_ERROR.FILE_NEED_EXTRACT_FIRST_ERROR, + message: 'file.error.needExtractFirst', + }); + } + + return; + } + + async validateExtract( + classTransform: T[], + filename: string + ): Promise { + const errors: IValidationErrorImport[] = []; + for (const [index, clsTransform] of classTransform.entries()) { + const validator: ValidationError[] = await validate( + clsTransform as Record + ); + if (validator.length > 0) { + errors.push({ + row: index, + file: filename, + errors: validator, + }); + } + } + + if (errors.length > 0) { + throw new UnprocessableEntityException({ + statusCode: + ENUM_FILE_STATUS_CODE_ERROR.FILE_VALIDATION_DTO_ERROR, + message: 'file.error.validationDto', + errors, + _errorType: 'import', + }); + } + + return; + } +} diff --git a/src/common/helper/constants/helper.enum.constant.ts b/src/common/helper/constants/helper.enum.constant.ts new file mode 100644 index 0000000..49a24a1 --- /dev/null +++ b/src/common/helper/constants/helper.enum.constant.ts @@ -0,0 +1,33 @@ +export enum ENUM_HELPER_DATE_FORMAT { + DATE = 'YYYY-MM-DD', + FRIENDLY_DATE = 'MMM, DD YYYY', + FRIENDLY_DATE_TIME = 'MMM, DD YYYY HH:MM:SS', + YEAR_MONTH = 'YYYY-MM', + MONTH_DATE = 'MM-DD', + ONLY_YEAR = 'YYYY', + ONLY_MONTH = 'MM', + ONLY_DATE = 'DD', + ISO_DATE = 'YYYY-MM-DDTHH:MM:SSZ', + DAY_LONG = 'dddd', + DAY_SHORT = 'ddd', + HOUR_LONG = 'HH', + HOUR_SHORT = 'H', + MINUTE_LONG = 'mm', + MINUTE_SHORT = 'm', + SECOND_LONG = 'ss', + SECOND_SHORT = 's', +} + +export enum ENUM_HELPER_DATE_DIFF { + MILIS = 'milis', + SECONDS = 'seconds', + HOURS = 'hours', + DAYS = 'days', + MINUTES = 'minutes', +} + +export enum ENUM_HELPER_FILE_TYPE { + XLSX = 'xlsx', + XLS = 'xls', + CSV = 'csv', +} diff --git a/src/common/helper/constants/helper.function.constant.ts b/src/common/helper/constants/helper.function.constant.ts new file mode 100644 index 0000000..0a0dc66 --- /dev/null +++ b/src/common/helper/constants/helper.function.constant.ts @@ -0,0 +1,5 @@ +import ms from 'ms'; + +export function seconds(msValue: string): number { + return ms(msValue) / 1000; +} diff --git a/src/common/helper/helper.module.ts b/src/common/helper/helper.module.ts new file mode 100644 index 0000000..4e9231b --- /dev/null +++ b/src/common/helper/helper.module.ts @@ -0,0 +1,53 @@ +import { Global, Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HelperArrayService } from './services/helper.array.service'; +import { HelperDateService } from './services/helper.date.service'; +import { HelperEncryptionService } from './services/helper.encryption.service'; +import { HelperHashService } from './services/helper.hash.service'; +import { HelperNumberService } from './services/helper.number.service'; +import { HelperStringService } from './services/helper.string.service'; +import { HelperFileService } from './services/helper.file.service'; +import { HelperGeoService } from './services/helper.geo.service'; + +@Global() +@Module({ + providers: [ + HelperArrayService, + HelperDateService, + HelperEncryptionService, + HelperHashService, + HelperNumberService, + HelperStringService, + HelperFileService, + HelperGeoService, + ], + exports: [ + HelperArrayService, + HelperDateService, + HelperEncryptionService, + HelperHashService, + HelperNumberService, + HelperStringService, + HelperFileService, + HelperGeoService, + ], + controllers: [], + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + secret: configService.get( + 'helper.jwt.defaultSecretKey' + ), + signOptions: { + expiresIn: configService.get( + 'helper.jwt.defaultExpirationTime' + ), + }, + }), + }), + ], +}) +export class HelperModule {} diff --git a/src/common/helper/interfaces/helper.array-service.interface.ts b/src/common/helper/interfaces/helper.array-service.interface.ts new file mode 100644 index 0000000..e5f8641 --- /dev/null +++ b/src/common/helper/interfaces/helper.array-service.interface.ts @@ -0,0 +1,57 @@ +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperArrayService { + getLeftByIndex(array: T[], index: number): T; + + getRightByIndex(array: T[], index: number): T; + + getLeftByLength(array: T[], length: number): T[]; + + getRightByLength(array: T[], length: number): T[]; + + getLast(array: T[]): T; + + getFirst(array: T[]): T; + + getFirstIndexByValue(array: T[], value: T): number; + + getLastIndexByValue(array: T[], value: T): number; + + removeByValue(array: T[], value: T): IHelperArrayRemove; + + removeLeftByLength(array: T[], length: number): T[]; + + removeRightByLength(array: Array, length: number): T[]; + + joinToString(array: Array, delimiter: string): string; + + reverse(array: T[]): T[]; + + unique(array: T[]): T[]; + + shuffle(array: T[]): T[]; + + merge(a: T[], b: T[]): T[]; + + mergeUnique(a: T[], b: T[]): T[]; + + filterIncludeByValue(array: T[], value: T): T[]; + + filterNotIncludeByValue(array: T[], value: T): T[]; + + filterNotIncludeUniqueByArray(a: T[], b: T[]): T[]; + + filterIncludeUniqueByArray(a: T[], b: T[]): T[]; + + equals(a: T[], b: T[]): boolean; + + notEquals(a: T[], b: T[]): boolean; + + in(a: T[], b: T[]): boolean; + + notIn(a: T[], b: T[]): boolean; + + includes(a: T[], b: T): boolean; + + chunk(a: T[], size: number): T[][]; +} diff --git a/src/common/helper/interfaces/helper.date-service.interface.ts b/src/common/helper/interfaces/helper.date-service.interface.ts new file mode 100644 index 0000000..29973b4 --- /dev/null +++ b/src/common/helper/interfaces/helper.date-service.interface.ts @@ -0,0 +1,99 @@ +import { + IHelperDateExtractDate, + IHelperDateOptionsBackward, + IHelperDateOptionsCreate, + IHelperDateOptionsDiff, + IHelperDateOptionsFormat, + IHelperDateOptionsForward, + IHelperDateStartAndEnd, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperDateService { + calculateAge(dateOfBirth: Date): number; + + diff( + dateOne: Date, + dateTwoMoreThanDateOne: Date, + options?: IHelperDateOptionsDiff + ): number; + + check(date: string | Date | number): boolean; + + checkTimestamp(timestamp: number): boolean; + + create( + date?: string | Date | number, + options?: IHelperDateOptionsCreate + ): Date; + + timestamp( + date?: string | Date | number, + options?: IHelperDateOptionsCreate + ): number; + + format(date: Date, options?: IHelperDateOptionsFormat): string; + + forwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInSeconds( + seconds: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInSeconds( + seconds: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInMinutes( + minutes: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInMinutes( + minutes: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInHours(hours: number, options?: IHelperDateOptionsForward): Date; + + backwardInHours(hours: number, options?: IHelperDateOptionsBackward): Date; + + forwardInDays(days: number, options?: IHelperDateOptionsForward): Date; + + backwardInDays(days: number, options?: IHelperDateOptionsBackward): Date; + + forwardInMonths(months: number, options?: IHelperDateOptionsForward): Date; + + backwardInMonths( + months: number, + options?: IHelperDateOptionsBackward + ): Date; + + endOfMonth(date?: Date): Date; + + startOfMonth(date?: Date): Date; + + endOfYear(date?: Date): Date; + + startOfYear(date?: Date): Date; + + endOfDay(date?: Date): Date; + + startOfDay(date?: Date): Date; + + extractDate(date: string | Date | number): IHelperDateExtractDate; + + getStartAndEndDate( + options?: IHelperDateStartAndEnd + ): IHelperDateStartAndEndDate; +} diff --git a/src/common/helper/interfaces/helper.encryption-service.interface.ts b/src/common/helper/interfaces/helper.encryption-service.interface.ts new file mode 100644 index 0000000..4c18ee2 --- /dev/null +++ b/src/common/helper/interfaces/helper.encryption-service.interface.ts @@ -0,0 +1,33 @@ +import { + IHelperJwtOptions, + IHelperJwtVerifyOptions, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperEncryptionService { + base64Encrypt(data: string): string; + + base64Decrypt(data: string): string; + + base64Compare(clientBasicToken: string, ourBasicToken: string): boolean; + + aes256Encrypt( + data: string | Record | Record[], + key: string, + iv: string + ): string; + + aes256Decrypt( + encrypted: string, + key: string, + iv: string + ): string | Record | Record[]; + + jwtEncrypt( + payload: Record, + options: IHelperJwtOptions + ): string; + + jwtDecrypt(token: string): Record; + + jwtVerify(token: string, options: IHelperJwtVerifyOptions): boolean; +} diff --git a/src/common/helper/interfaces/helper.file-service.interface.ts b/src/common/helper/interfaces/helper.file-service.interface.ts new file mode 100644 index 0000000..0479b01 --- /dev/null +++ b/src/common/helper/interfaces/helper.file-service.interface.ts @@ -0,0 +1,26 @@ +import { + IHelperFileWriteExcelOptions, + IHelperFileReadExcelOptions, + IHelperFileRows, + IHelperFileCreateExcelWorkbookOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { WorkBook } from 'xlsx'; + +export interface IHelperFileService { + createExcelWorkbook( + rows: IHelperFileRows[], + options?: IHelperFileCreateExcelWorkbookOptions + ): WorkBook; + + writeExcelToBuffer( + workbook: WorkBook, + options?: IHelperFileWriteExcelOptions + ): Buffer; + + readExcelFromBuffer( + file: Buffer, + options?: IHelperFileReadExcelOptions + ): IHelperFileRows[]; + + convertToBytes(megabytes: string): number; +} diff --git a/src/common/helper/interfaces/helper.geo-service.interface.ts b/src/common/helper/interfaces/helper.geo-service.interface.ts new file mode 100644 index 0000000..60f05b4 --- /dev/null +++ b/src/common/helper/interfaces/helper.geo-service.interface.ts @@ -0,0 +1,8 @@ +import { + IHelperGeoCurrent, + IHelperGeoRules, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperGeoService { + inRadius(geoRule: IHelperGeoRules, geoCurrent: IHelperGeoCurrent): boolean; +} diff --git a/src/common/helper/interfaces/helper.hash-service.interface.ts b/src/common/helper/interfaces/helper.hash-service.interface.ts new file mode 100644 index 0000000..52d890d --- /dev/null +++ b/src/common/helper/interfaces/helper.hash-service.interface.ts @@ -0,0 +1,11 @@ +export interface IHelperHashService { + randomSalt(length: number): string; + + bcrypt(passwordString: string, salt: string): string; + + bcryptCompare(passwordString: string, passwordHashed: string): boolean; + + sha256(string: string): string; + + sha256Compare(hashOne: string, hashTwo: string): boolean; +} diff --git a/src/common/helper/interfaces/helper.interface.ts b/src/common/helper/interfaces/helper.interface.ts new file mode 100644 index 0000000..7c67376 --- /dev/null +++ b/src/common/helper/interfaces/helper.interface.ts @@ -0,0 +1,101 @@ +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, + ENUM_HELPER_FILE_TYPE, +} from 'src/common/helper/constants/helper.enum.constant'; + +// Helper Array +export interface IHelperArrayRemove { + removed: T[]; + arrays: T[]; +} + +// Helper Encryption +export interface IHelperJwtVerifyOptions { + audience: string; + issuer: string; + subject: string; + secretKey: string; +} + +export interface IHelperJwtOptions extends IHelperJwtVerifyOptions { + expiredIn: number | string; + notBefore?: number | string; +} + +// Helper String +export interface IHelperStringRandomOptions { + upperCase?: boolean; + safe?: boolean; + prefix?: string; +} + +// Helper Geo +export interface IHelperGeoCurrent { + latitude: number; + longitude: number; +} + +export interface IHelperGeoRules extends IHelperGeoCurrent { + radiusInMeters: number; +} + +// Helper Date +export interface IHelperDateStartAndEnd { + month?: number; + year?: number; +} + +export interface IHelperDateStartAndEndDate { + startDate: Date; + endDate: Date; +} + +export interface IHelperDateExtractDate { + date: Date; + day: string; + month: string; + year: string; +} + +export interface IHelperDateOptionsDiff { + format?: ENUM_HELPER_DATE_DIFF; +} + +export interface IHelperDateOptionsCreate { + startOfDay?: boolean; +} + +export interface IHelperDateOptionsFormat { + format?: ENUM_HELPER_DATE_FORMAT | string; +} + +export interface IHelperDateOptionsForward { + fromDate?: Date; +} + +export type IHelperDateOptionsBackward = IHelperDateOptionsForward; + +export interface IHelperDateOptionsRoundDown { + hour: boolean; + minute: boolean; + second: boolean; +} + +// Helper File + +export type IHelperFileRows = Record; + +export interface IHelperFileWriteExcelOptions { + password?: string; + type?: ENUM_HELPER_FILE_TYPE; +} + +export interface IHelperFileCreateExcelWorkbookOptions { + sheetName?: string; +} + +export interface IHelperFileReadExcelOptions { + sheet?: string | number; + password?: string; +} diff --git a/src/common/helper/interfaces/helper.number-service.interface.ts b/src/common/helper/interfaces/helper.number-service.interface.ts new file mode 100644 index 0000000..7fe5a46 --- /dev/null +++ b/src/common/helper/interfaces/helper.number-service.interface.ts @@ -0,0 +1,11 @@ +export interface IHelperNumberService { + check(number: string): boolean; + + create(number: string): number; + + random(length: number): number; + + randomInRange(min: number, max: number): number; + + percent(value: number, total: number): number; +} diff --git a/src/common/helper/interfaces/helper.string-service.interface.ts b/src/common/helper/interfaces/helper.string-service.interface.ts new file mode 100644 index 0000000..b743edc --- /dev/null +++ b/src/common/helper/interfaces/helper.string-service.interface.ts @@ -0,0 +1,19 @@ +import { IHelperStringRandomOptions } from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperStringService { + checkEmail(email: string): boolean; + + randomReference(length: number, prefix?: string): string; + + random(length: number, options?: IHelperStringRandomOptions): string; + + censor(value: string): string; + + checkPasswordWeak(password: string, length?: number): boolean; + + checkPasswordMedium(password: string, length?: number): boolean; + + checkPasswordStrong(password: string, length?: number): boolean; + + checkSafeString(text: string): boolean; +} diff --git a/src/common/helper/services/helper.array.service.ts b/src/common/helper/services/helper.array.service.ts new file mode 100644 index 0000000..dd4b2bf --- /dev/null +++ b/src/common/helper/services/helper.array.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import _ from 'lodash'; +import { IHelperArrayService } from 'src/common/helper/interfaces/helper.array-service.interface'; +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperArrayService implements IHelperArrayService { + getLeftByIndex(array: T[], index: number): T { + return _.nth(array, index); + } + + getRightByIndex(array: T[], index: number): T { + return _.nth(array, -Math.abs(index)); + } + + getLeftByLength(array: T[], length: number): T[] { + return _.take(array, length); + } + + getRightByLength(array: T[], length: number): T[] { + return _.takeRight(array, length); + } + + getLast(array: T[]): T { + return _.last(array); + } + + getFirst(array: T[]): T { + return _.head(array); + } + + getFirstIndexByValue(array: T[], value: T): number { + return _.indexOf(array, value); + } + + getLastIndexByValue(array: T[], value: T): number { + return _.lastIndexOf(array, value); + } + + removeByValue(array: T[], value: T): IHelperArrayRemove { + const removed = _.remove(array, function (n) { + return n === value; + }); + + return { removed, arrays: array }; + } + + removeLeftByLength(array: T[], length: number): T[] { + return _.drop(array, length); + } + + removeRightByLength(array: Array, length: number): T[] { + return _.dropRight(array, length); + } + + joinToString(array: Array, delimiter: string): string { + return _.join(array, delimiter); + } + + reverse(array: T[]): T[] { + return _.reverse(array); + } + + unique(array: T[]): T[] { + return _.uniq(array); + } + + shuffle(array: T[]): T[] { + return _.shuffle(array); + } + + merge(a: T[], b: T[]): T[] { + return _.concat(a, b); + } + + mergeUnique(a: T[], b: T[]): T[] { + return _.union(a, b); + } + + filterIncludeByValue(array: T[], value: T): T[] { + return _.filter(array, (arr) => arr === value); + } + + filterNotIncludeByValue(array: T[], value: T): T[] { + return _.without(array, value); + } + + filterNotIncludeUniqueByArray(a: T[], b: T[]): T[] { + return _.xor(a, b); + } + + filterIncludeUniqueByArray(a: T[], b: T[]): T[] { + return _.intersection(a, b); + } + + equals(a: T[], b: T[]): boolean { + return _.isEqual(a, b); + } + + notEquals(a: T[], b: T[]): boolean { + return !_.isEqual(a, b); + } + + in(a: T[], b: T[]): boolean { + return _.intersection(a, b).length > 0; + } + + notIn(a: T[], b: T[]): boolean { + return _.intersection(a, b).length == 0; + } + + includes(a: T[], b: T): boolean { + return _.includes(a, b); + } + + chunk(a: T[], size: number): T[][] { + return _.chunk(a, size); + } +} diff --git a/src/common/helper/services/helper.date.service.ts b/src/common/helper/services/helper.date.service.ts new file mode 100644 index 0000000..6a74b9f --- /dev/null +++ b/src/common/helper/services/helper.date.service.ts @@ -0,0 +1,244 @@ +import { Injectable } from '@nestjs/common'; +import moment from 'moment'; +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, +} from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperDateService } from 'src/common/helper/interfaces/helper.date-service.interface'; +import { + IHelperDateExtractDate, + IHelperDateOptionsBackward, + IHelperDateOptionsCreate, + IHelperDateOptionsDiff, + IHelperDateOptionsFormat, + IHelperDateOptionsForward, + IHelperDateOptionsRoundDown, + IHelperDateStartAndEnd, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperDateService implements IHelperDateService { + calculateAge(dateOfBirth: Date): number { + return moment().diff(dateOfBirth, 'years'); + } + + diff( + dateOne: Date, + dateTwoMoreThanDateOne: Date, + options?: IHelperDateOptionsDiff + ): number { + const mDateOne = moment(dateOne); + const mDateTwo = moment(dateTwoMoreThanDateOne); + const diff = moment.duration(mDateTwo.diff(mDateOne)); + + if (options?.format === ENUM_HELPER_DATE_DIFF.MILIS) { + return diff.asMilliseconds(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.SECONDS) { + return diff.asSeconds(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.HOURS) { + return diff.asHours(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.MINUTES) { + return diff.asMinutes(); + } else { + return diff.asDays(); + } + } + + check(date: string | Date | number): boolean { + return moment(date, true).isValid(); + } + + checkTimestamp(timestamp: number): boolean { + return moment(timestamp, true).isValid(); + } + + create( + date?: string | number | Date, + options?: IHelperDateOptionsCreate + ): Date { + const mDate = moment(date ?? undefined); + + if (options?.startOfDay) { + mDate.startOf('day'); + } + + return mDate.toDate(); + } + + timestamp( + date?: string | number | Date, + options?: IHelperDateOptionsCreate + ): number { + const mDate = moment(date ?? undefined); + + if (options?.startOfDay) { + mDate.startOf('day'); + } + + return mDate.valueOf(); + } + + format(date: Date, options?: IHelperDateOptionsFormat): string { + return moment(date).format( + options?.format ?? ENUM_HELPER_DATE_FORMAT.DATE + ); + } + + forwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(milliseconds, 'ms').toDate(); + } + + backwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(milliseconds, 'ms').toDate(); + } + + forwardInSeconds( + seconds: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(seconds, 's').toDate(); + } + + backwardInSeconds( + seconds: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(seconds, 's').toDate(); + } + + forwardInMinutes( + minutes: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(minutes, 'm').toDate(); + } + + backwardInMinutes( + minutes: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(minutes, 'm').toDate(); + } + + forwardInHours(hours: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(hours, 'h').toDate(); + } + + backwardInHours(hours: number, options?: IHelperDateOptionsBackward): Date { + return moment(options?.fromDate).subtract(hours, 'h').toDate(); + } + + forwardInDays(days: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(days, 'd').toDate(); + } + + backwardInDays(days: number, options?: IHelperDateOptionsBackward): Date { + return moment(options?.fromDate).subtract(days, 'd').toDate(); + } + + forwardInMonths(months: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(months, 'M').toDate(); + } + + backwardInMonths( + months: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(months, 'M').toDate(); + } + + endOfMonth(date?: Date): Date { + return moment(date).endOf('month').toDate(); + } + + startOfMonth(date?: Date): Date { + return moment(date).startOf('month').toDate(); + } + + endOfYear(date?: Date): Date { + return moment(date).endOf('year').toDate(); + } + + startOfYear(date?: Date): Date { + return moment(date).startOf('year').toDate(); + } + + endOfDay(date?: Date): Date { + return moment(date).endOf('day').toDate(); + } + + startOfDay(date?: Date): Date { + return moment(date).startOf('day').toDate(); + } + + extractDate(date: string | Date | number): IHelperDateExtractDate { + const newDate = this.create(date); + const day: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_DATE, + }); + const month: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_MONTH, + }); + const year: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_YEAR, + }); + + return { + date: newDate, + day, + month, + year, + }; + } + + roundDown(date: Date, options?: IHelperDateOptionsRoundDown): Date { + const mDate = moment(date).set({ millisecond: 0 }); + + if (options?.hour) { + mDate.set({ hour: 0 }); + } + + if (options?.minute) { + mDate.set({ minute: 0 }); + } + + if (options?.second) { + mDate.set({ second: 0 }); + } + + return mDate.toDate(); + } + + getStartAndEndDate( + options?: IHelperDateStartAndEnd + ): IHelperDateStartAndEndDate { + const today = moment(); + const todayMonth = today.format(ENUM_HELPER_DATE_FORMAT.ONLY_MONTH); + const todayYear = today.format(ENUM_HELPER_DATE_FORMAT.ONLY_YEAR); + // set month and year + const year = options?.year ?? todayYear; + const month = options?.month ?? todayMonth; + + const date = moment(`${year}-${month}-02`, 'YYYY-MM-DD'); + let startDate: Date = date.startOf('year').toDate(); + let endDate: Date = date.endOf('year').toDate(); + + if (options?.month) { + const date = moment(`${year}-${month}-02`, 'YYYY-MM-DD'); + startDate = date.startOf('month').toDate(); + endDate = date.endOf('month').toDate(); + } + + return { + startDate, + endDate, + }; + } +} diff --git a/src/common/helper/services/helper.encryption.service.ts b/src/common/helper/services/helper.encryption.service.ts new file mode 100644 index 0000000..347997a --- /dev/null +++ b/src/common/helper/services/helper.encryption.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AES, enc, mode, pad } from 'crypto-js'; +import { IHelperEncryptionService } from 'src/common/helper/interfaces/helper.encryption-service.interface'; +import { + IHelperJwtOptions, + IHelperJwtVerifyOptions, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperEncryptionService implements IHelperEncryptionService { + constructor(private readonly jwtService: JwtService) {} + + base64Encrypt(data: string): string { + const buff: Buffer = Buffer.from(data, 'utf8'); + return buff.toString('base64'); + } + + base64Decrypt(data: string): string { + const buff: Buffer = Buffer.from(data, 'base64'); + return buff.toString('utf8'); + } + + base64Compare(clientBasicToken: string, ourBasicToken: string): boolean { + return ourBasicToken === clientBasicToken; + } + + aes256Encrypt( + data: string | Record | Record[], + key: string, + iv: string + ): string { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.encrypt(JSON.stringify(data), key, { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return cipher.toString(); + } + + aes256Decrypt( + encrypted: string, + key: string, + iv: string + ): string | Record | Record[] { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.decrypt(encrypted, key, { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return JSON.parse(cipher.toString(enc.Utf8)); + } + + jwtEncrypt( + payload: Record, + options: IHelperJwtOptions + ): string { + return this.jwtService.sign(payload, { + secret: options.secretKey, + expiresIn: options.expiredIn, + notBefore: options.notBefore ?? 0, + audience: options.audience, + issuer: options.issuer, + subject: options.subject, + }); + } + + jwtDecrypt(token: string): Record { + return this.jwtService.decode(token) as Record; + } + + jwtVerify(token: string, options: IHelperJwtVerifyOptions): boolean { + try { + this.jwtService.verify(token, { + secret: options.secretKey, + audience: options.audience, + issuer: options.issuer, + subject: options.subject, + }); + return true; + } catch (err: unknown) { + return false; + } + } +} diff --git a/src/common/helper/services/helper.file.service.ts b/src/common/helper/services/helper.file.service.ts new file mode 100644 index 0000000..03244a3 --- /dev/null +++ b/src/common/helper/services/helper.file.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@nestjs/common'; +import bytes from 'bytes'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperFileService } from 'src/common/helper/interfaces/helper.file-service.interface'; +import { + IHelperFileWriteExcelOptions, + IHelperFileReadExcelOptions, + IHelperFileRows, + IHelperFileCreateExcelWorkbookOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { utils, write, read, WorkBook } from 'xlsx'; + +@Injectable() +export class HelperFileService implements IHelperFileService { + createExcelWorkbook( + rows: IHelperFileRows[], + options?: IHelperFileCreateExcelWorkbookOptions + ): WorkBook { + // headers + const headers = rows.length > 0 ? Object.keys(rows[0]) : []; + + // worksheet + const worksheet = utils.json_to_sheet(rows); + + // workbook + const workbook = utils.book_new(); + + utils.sheet_add_aoa(worksheet, [headers], { origin: 'A1' }); + utils.book_append_sheet( + workbook, + worksheet, + options?.sheetName ?? 'Sheet 1' + ); + + return workbook; + } + + writeExcelToBuffer( + workbook: WorkBook, + options?: IHelperFileWriteExcelOptions + ): Buffer { + // create buffer + const buff: Buffer = write(workbook, { + type: 'buffer', + bookType: options?.type ?? ENUM_HELPER_FILE_TYPE.CSV, + password: options?.password, + }); + + return buff; + } + + readExcelFromBuffer( + file: Buffer, + options?: IHelperFileReadExcelOptions + ): IHelperFileRows[] { + // workbook + const workbook = read(file, { + type: 'buffer', + password: options?.password, + sheets: options?.sheet, + }); + + // worksheet + const worksheetName = workbook.SheetNames; + const worksheet = workbook.Sheets[worksheetName[0]]; + + // rows + const rows: IHelperFileRows[] = utils.sheet_to_json(worksheet); + + return rows; + } + + convertToBytes(megabytes: string): number { + return bytes(megabytes); + } +} diff --git a/src/common/helper/services/helper.geo.service.ts b/src/common/helper/services/helper.geo.service.ts new file mode 100644 index 0000000..deaad14 --- /dev/null +++ b/src/common/helper/services/helper.geo.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { isPointWithinRadius } from 'geolib'; +import { IHelperGeoService } from 'src/common/helper/interfaces/helper.geo-service.interface'; +import { + IHelperGeoCurrent, + IHelperGeoRules, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperGeoService implements IHelperGeoService { + inRadius(geoRule: IHelperGeoRules, geoCurrent: IHelperGeoCurrent): boolean { + return isPointWithinRadius( + { latitude: geoRule.latitude, longitude: geoRule.longitude }, + geoCurrent, + geoRule.radiusInMeters + ); + } +} diff --git a/src/common/helper/services/helper.hash.service.ts b/src/common/helper/services/helper.hash.service.ts new file mode 100644 index 0000000..c7c9987 --- /dev/null +++ b/src/common/helper/services/helper.hash.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { compareSync, genSaltSync, hashSync } from 'bcryptjs'; +import { SHA256, enc } from 'crypto-js'; +import { IHelperHashService } from 'src/common/helper/interfaces/helper.hash-service.interface'; + +@Injectable() +export class HelperHashService implements IHelperHashService { + randomSalt(length: number): string { + return genSaltSync(length); + } + + bcrypt(passwordString: string, salt: string): string { + return hashSync(passwordString, salt); + } + + bcryptCompare(passwordString: string, passwordHashed: string): boolean { + return compareSync(passwordString, passwordHashed); + } + + sha256(string: string): string { + return SHA256(string).toString(enc.Hex); + } + + sha256Compare(hashOne: string, hashTwo: string): boolean { + return hashOne === hashTwo; + } +} diff --git a/src/common/helper/services/helper.number.service.ts b/src/common/helper/services/helper.number.service.ts new file mode 100644 index 0000000..f34224a --- /dev/null +++ b/src/common/helper/services/helper.number.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { faker } from '@faker-js/faker'; +import { IHelperNumberService } from 'src/common/helper/interfaces/helper.number-service.interface'; + +@Injectable() +export class HelperNumberService implements IHelperNumberService { + check(number: string): boolean { + const regex = /^-?\d+$/; + return regex.test(number); + } + + create(number: string): number { + return Number(number); + } + + random(length: number): number { + const min: number = Number.parseInt(`1`.padEnd(length, '0')); + const max: number = Number.parseInt(`9`.padEnd(length, '9')); + return this.randomInRange(min, max); + } + + randomInRange(min: number, max: number): number { + return faker.datatype.number({ min, max }); + } + + percent(value: number, total: number): number { + let tValue = value / total; + if (Number.isNaN(tValue) || !Number.isFinite(tValue)) { + tValue = 0; + } + return Number.parseFloat((tValue * 100).toFixed(2)); + } +} diff --git a/src/common/helper/services/helper.string.service.ts b/src/common/helper/services/helper.string.service.ts new file mode 100644 index 0000000..caeb372 --- /dev/null +++ b/src/common/helper/services/helper.string.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { faker } from '@faker-js/faker'; +import { HelperDateService } from './helper.date.service'; +import { IHelperStringRandomOptions } from 'src/common/helper/interfaces/helper.interface'; +import { IHelperStringService } from 'src/common/helper/interfaces/helper.string-service.interface'; + +@Injectable() +export class HelperStringService implements IHelperStringService { + constructor(private readonly helperDateService: HelperDateService) {} + + checkEmail(email: string): boolean { + const regex = /\S+@\S+\.\S+/; + return regex.test(email); + } + + randomReference(length: number, prefix?: string): string { + const timestamp = `${this.helperDateService.timestamp()}`; + const randomString: string = this.random(length, { + safe: true, + upperCase: true, + }); + + return prefix + ? `${prefix}-${timestamp}${randomString}` + : `${timestamp}${randomString}`; + } + + random(length: number, options?: IHelperStringRandomOptions): string { + const rString = options?.safe + ? faker.internet.password(length, true, /[A-Z]/, options?.prefix) + : faker.internet.password(length, false, /\w/, options?.prefix); + + return options?.upperCase ? rString.toUpperCase() : rString; + } + + censor(value: string): string { + const length = value.length; + if (length === 1) { + return value; + } + + const end = length > 4 ? length - 4 : 1; + const censorString = '*'.repeat(end > 10 ? 10 : end); + const visibleString = value.substring(end, length); + return `${censorString}${visibleString}`; + } + + checkPasswordWeak(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z]).{${length ?? 8},}$` + ); + + return regex.test(password); + } + + checkPasswordMedium(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{${length ?? 8},}$` + ); + + return regex.test(password); + } + + checkPasswordStrong(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{${ + length ?? 8 + },}$` + ); + + return regex.test(password); + } + + checkSafeString(text: string): boolean { + const regex = new RegExp('^[A-Za-z0-9_-]+$'); + return regex.test(text); + } +} diff --git a/src/common/logger/constants/logger.constant.ts b/src/common/logger/constants/logger.constant.ts new file mode 100644 index 0000000..7a9a388 --- /dev/null +++ b/src/common/logger/constants/logger.constant.ts @@ -0,0 +1,2 @@ +export const LOGGER_ACTION_META_KEY = 'LoggerActionMetaKey'; +export const LOGGER_OPTIONS_META_KEY = 'LoggerOptionsMetaKey'; diff --git a/src/common/logger/constants/logger.enum.constant.ts b/src/common/logger/constants/logger.enum.constant.ts new file mode 100644 index 0000000..6ff677a --- /dev/null +++ b/src/common/logger/constants/logger.enum.constant.ts @@ -0,0 +1,11 @@ +export enum ENUM_LOGGER_LEVEL { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARM = 'WARM', + FATAL = 'FATAL', +} + +export enum ENUM_LOGGER_ACTION { + LOGIN = 'LOGIN', + TEST = 'TEST', +} diff --git a/src/common/logger/decorators/logger.decorator.ts b/src/common/logger/decorators/logger.decorator.ts new file mode 100644 index 0000000..4fc569b --- /dev/null +++ b/src/common/logger/decorators/logger.decorator.ts @@ -0,0 +1,19 @@ +import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common'; +import { + LOGGER_ACTION_META_KEY, + LOGGER_OPTIONS_META_KEY, +} from 'src/common/logger/constants/logger.constant'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { LoggerInterceptor } from 'src/common/logger/interceptors/logger.interceptor'; +import { ILoggerOptions } from 'src/common/logger/interfaces/logger.interface'; + +export function Logger( + action: ENUM_LOGGER_ACTION, + options?: ILoggerOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(LoggerInterceptor), + SetMetadata(LOGGER_ACTION_META_KEY, action), + SetMetadata(LOGGER_OPTIONS_META_KEY, options ?? {}) + ); +} diff --git a/src/common/logger/dtos/logger.create.dto.ts b/src/common/logger/dtos/logger.create.dto.ts new file mode 100644 index 0000000..ae42558 --- /dev/null +++ b/src/common/logger/dtos/logger.create.dto.ts @@ -0,0 +1,26 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; + +export class LoggerCreateDto { + action: ENUM_LOGGER_ACTION; + description: string; + apiKey?: string; + user?: string; + requestId?: string; + method: ENUM_REQUEST_METHOD; + path: string; + role?: string; + accessFor?: ENUM_AUTH_ACCESS_FOR; + tags?: string[]; + params?: Record; + bodies?: Record; + statusCode?: number; +} + +export class LoggerCreateRawDto extends LoggerCreateDto { + level: ENUM_LOGGER_LEVEL; +} diff --git a/src/common/logger/interceptors/logger.interceptor.ts b/src/common/logger/interceptors/logger.interceptor.ts new file mode 100644 index 0000000..7571dd9 --- /dev/null +++ b/src/common/logger/interceptors/logger.interceptor.ts @@ -0,0 +1,91 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; +import { Response } from 'express'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Reflector } from '@nestjs/core'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { LoggerService } from 'src/common/logger/services/logger.service'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { + LOGGER_ACTION_META_KEY, + LOGGER_OPTIONS_META_KEY, +} from 'src/common/logger/constants/logger.constant'; +import { ILoggerOptions } from 'src/common/logger/interfaces/logger.interface'; + +@Injectable() +export class LoggerInterceptor implements NestInterceptor { + constructor( + private readonly reflector: Reflector, + private readonly loggerService: LoggerService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const { + apiKey, + method, + originalUrl, + user, + __id, + body, + params, + path, + } = ctx.getRequest(); + const responseExpress = ctx.getResponse(); + + return next.handle().pipe( + tap(async (response: Promise>) => { + const responseData: Record = await response; + const responseStatus: number = responseExpress.statusCode; + const statusCode = + responseData?.statusCode ?? responseStatus; + + const loggerAction: ENUM_LOGGER_ACTION = + this.reflector.get( + LOGGER_ACTION_META_KEY, + context.getHandler() + ); + const loggerOptions: ILoggerOptions = + this.reflector.get( + LOGGER_OPTIONS_META_KEY, + context.getHandler() + ); + + await this.loggerService.raw({ + level: loggerOptions?.level ?? ENUM_LOGGER_LEVEL.INFO, + action: loggerAction, + description: + loggerOptions?.description ?? + `Request ${method} called, url ${originalUrl}, and action ${loggerAction}`, + apiKey: apiKey?._id, + user: user?._id, + requestId: __id, + method: method as ENUM_REQUEST_METHOD, + role: user?.role, + accessFor: user?.accessFor, + params, + bodies: body, + path, + statusCode, + tags: loggerOptions?.tags ?? [], + }); + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/logger/interfaces/logger.interface.ts b/src/common/logger/interfaces/logger.interface.ts new file mode 100644 index 0000000..d2fc900 --- /dev/null +++ b/src/common/logger/interfaces/logger.interface.ts @@ -0,0 +1,7 @@ +import { ENUM_LOGGER_LEVEL } from 'src/common/logger/constants/logger.enum.constant'; + +export interface ILoggerOptions { + description?: string; + tags?: string[]; + level?: ENUM_LOGGER_LEVEL; +} diff --git a/src/common/logger/interfaces/logger.service.interface.ts b/src/common/logger/interfaces/logger.service.interface.ts new file mode 100644 index 0000000..68711e4 --- /dev/null +++ b/src/common/logger/interfaces/logger.service.interface.ts @@ -0,0 +1,17 @@ +import { + LoggerCreateDto, + LoggerCreateRawDto, +} from 'src/common/logger/dtos/logger.create.dto'; +import { LoggerDoc } from 'src/common/logger/repository/entities/logger.entity'; + +export interface ILoggerService { + info(data: LoggerCreateDto): Promise; + + debug(data: LoggerCreateDto): Promise; + + warn(data: LoggerCreateDto): Promise; + + fatal(data: LoggerCreateDto): Promise; + + raw(data: LoggerCreateRawDto): Promise; +} diff --git a/src/common/logger/logger.module.ts b/src/common/logger/logger.module.ts new file mode 100644 index 0000000..c834b22 --- /dev/null +++ b/src/common/logger/logger.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { LoggerRepositoryModule } from 'src/common/logger/repository/logger.repository.module'; +import { LoggerService } from './services/logger.service'; + +@Global() +@Module({ + providers: [LoggerService], + exports: [LoggerService], + imports: [LoggerRepositoryModule], +}) +export class LoggerModule {} diff --git a/src/common/logger/repository/entities/logger.entity.ts b/src/common/logger/repository/entities/logger.entity.ts new file mode 100644 index 0000000..89ebd79 --- /dev/null +++ b/src/common/logger/repository/entities/logger.entity.ts @@ -0,0 +1,117 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import { Document } from 'mongoose'; + +export const LoggerDatabaseName = 'loggers'; + +@DatabaseEntity({ collection: LoggerDatabaseName }) +export class LoggerEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + enum: ENUM_LOGGER_LEVEL, + type: String, + }) + level: string; + + @Prop({ + required: true, + enum: ENUM_LOGGER_ACTION, + type: String, + }) + action: string; + + @Prop({ + required: true, + enum: ENUM_REQUEST_METHOD, + type: String, + }) + method: string; + + @Prop({ + required: false, + type: String, + }) + requestId?: string; + + @Prop({ + required: false, + type: String, + }) + user?: string; + + @Prop({ + required: false, + type: String, + }) + role?: string; + + @Prop({ + required: false, + ref: ApiKeyEntity.name, + type: String, + }) + apiKey?: string; + + @Prop({ + required: true, + default: true, + type: Boolean, + }) + anonymous: boolean; + + @Prop({ + required: false, + enum: ENUM_AUTH_ACCESS_FOR, + type: String, + }) + accessFor?: ENUM_AUTH_ACCESS_FOR; + + @Prop({ + required: true, + type: String, + }) + description: string; + + @Prop({ + required: false, + type: Object, + }) + params?: Record; + + @Prop({ + required: false, + type: Object, + }) + bodies?: Record; + + @Prop({ + required: false, + type: Number, + }) + statusCode?: number; + + @Prop({ + required: false, + type: String, + }) + path?: string; + + @Prop({ + required: false, + default: [], + type: Array, + }) + tags: string[]; +} + +export const LoggerSchema = SchemaFactory.createForClass(LoggerEntity); + +export type LoggerDoc = LoggerEntity & Document; diff --git a/src/common/logger/repository/logger.repository.module.ts b/src/common/logger/repository/logger.repository.module.ts new file mode 100644 index 0000000..890b413 --- /dev/null +++ b/src/common/logger/repository/logger.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + LoggerEntity, + LoggerSchema, +} from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerRepository } from 'src/common/logger/repository/repositories/logger.repository'; + +@Module({ + providers: [LoggerRepository], + exports: [LoggerRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: LoggerEntity.name, + schema: LoggerSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class LoggerRepositoryModule {} diff --git a/src/common/logger/repository/repositories/logger.repository.ts b/src/common/logger/repository/repositories/logger.repository.ts new file mode 100644 index 0000000..adb1497 --- /dev/null +++ b/src/common/logger/repository/repositories/logger.repository.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { + LoggerDoc, + LoggerEntity, +} from 'src/common/logger/repository/entities/logger.entity'; + +@Injectable() +export class LoggerRepository extends DatabaseMongoUUIDRepositoryAbstract< + LoggerEntity, + LoggerDoc +> { + constructor( + @DatabaseModel(LoggerEntity.name) + private readonly LoggerDoc: Model + ) { + super(LoggerDoc, { + path: 'apiKey', + match: '_id', + model: ApiKeyEntity.name, + }); + } +} diff --git a/src/common/logger/services/logger.service.ts b/src/common/logger/services/logger.service.ts new file mode 100644 index 0000000..2379a8b --- /dev/null +++ b/src/common/logger/services/logger.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@nestjs/common'; +import { ENUM_LOGGER_LEVEL } from 'src/common/logger/constants/logger.enum.constant'; +import { + LoggerCreateDto, + LoggerCreateRawDto, +} from 'src/common/logger/dtos/logger.create.dto'; +import { ILoggerService } from 'src/common/logger/interfaces/logger.service.interface'; +import { + LoggerDoc, + LoggerEntity, +} from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerRepository } from 'src/common/logger/repository/repositories/logger.repository'; + +@Injectable() +export class LoggerService implements ILoggerService { + constructor(private readonly loggerRepository: LoggerRepository) {} + + async info({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.INFO; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async debug({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.DEBUG; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async warn({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.WARM; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async fatal({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.FATAL; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async raw({ + level, + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateRawDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = level; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } +} diff --git a/src/common/message/constants/message.enum.constant.ts b/src/common/message/constants/message.enum.constant.ts new file mode 100644 index 0000000..f4e18bc --- /dev/null +++ b/src/common/message/constants/message.enum.constant.ts @@ -0,0 +1,4 @@ +export enum ENUM_MESSAGE_LANGUAGE { + EN = 'en', + ID = 'id', +} diff --git a/src/common/message/controllers/message.controller.ts b/src/common/message/controllers/message.controller.ts new file mode 100644 index 0000000..96032cd --- /dev/null +++ b/src/common/message/controllers/message.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { MessageEnumLanguageDoc } from 'src/common/message/docs/message.enum.doc'; +import { MessageLanguageSerialization } from 'src/common/message/serializations/message.language.serialization'; +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'; + +@ApiTags('message') +@Controller({ + version: VERSION_NEUTRAL, + path: '/message', +}) +export class MessageController { + constructor(private readonly messageService: MessageService) {} + + @MessageEnumLanguageDoc() + @Response('message.languages', { + serialization: MessageLanguageSerialization, + }) + @Get('/languages') + async languages(): Promise { + const languages: string[] = + await this.messageService.getAvailableLanguages(); + + return { + data: { languages }, + }; + } +} diff --git a/src/common/message/docs/message.enum.doc.ts b/src/common/message/docs/message.enum.doc.ts new file mode 100644 index 0000000..e5742bf --- /dev/null +++ b/src/common/message/docs/message.enum.doc.ts @@ -0,0 +1,11 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { MessageLanguageSerialization } from 'src/common/message/serializations/message.language.serialization'; + +export function MessageEnumLanguageDoc(): MethodDecorator { + return applyDecorators( + Doc('message.languages', { + response: { serialization: MessageLanguageSerialization }, + }) + ); +} diff --git a/src/common/message/interfaces/message.interface.ts b/src/common/message/interfaces/message.interface.ts new file mode 100644 index 0000000..210b335 --- /dev/null +++ b/src/common/message/interfaces/message.interface.ts @@ -0,0 +1,9 @@ +export type IMessage = Record; + +export type IMessageOptionsProperties = Record; +export interface IMessageOptions { + readonly customLanguages?: string[]; + readonly properties?: IMessageOptionsProperties; +} + +export type IMessageSetOptions = Omit; diff --git a/src/common/message/interfaces/message.service.interface.ts b/src/common/message/interfaces/message.service.interface.ts new file mode 100644 index 0000000..4c2047a --- /dev/null +++ b/src/common/message/interfaces/message.service.interface.ts @@ -0,0 +1,33 @@ +import { ValidationError } from '@nestjs/common'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { + IMessage, + IMessageOptions, + IMessageSetOptions, +} from 'src/common/message/interfaces/message.interface'; + +export interface IMessageService { + getAvailableLanguages(): Promise; + + setMessage( + lang: string, + key: string, + options?: IMessageSetOptions + ): T; + + getRequestErrorsMessage( + requestErrors: ValidationError[], + customLanguages?: string[] + ): Promise; + + getImportErrorsMessage( + errors: IValidationErrorImport[], + customLanguages?: string[] + ): Promise; + + get(key: string, options?: IMessageOptions): Promise; +} diff --git a/src/common/message/message.module.ts b/src/common/message/message.module.ts new file mode 100644 index 0000000..5e200ad --- /dev/null +++ b/src/common/message/message.module.ts @@ -0,0 +1,36 @@ +import { Global, Module } from '@nestjs/common'; +import * as path from 'path'; +import { I18nModule, HeaderResolver, I18nJsonLoader } from 'nestjs-i18n'; +import { ConfigService } from '@nestjs/config'; +import { MessageService } from './services/message.service'; +import { ENUM_MESSAGE_LANGUAGE } from './constants/message.enum.constant'; +import { MessageMiddlewareModule } from 'src/common/message/middleware/message.middleware.module'; + +@Global() +@Module({ + providers: [MessageService], + exports: [MessageService], + imports: [ + I18nModule.forRootAsync({ + useFactory: (configService: ConfigService) => ({ + fallbackLanguage: configService + .get('app.language') + .join(','), + fallbacks: Object.values(ENUM_MESSAGE_LANGUAGE).reduce( + (a, v) => ({ ...a, [`${v}-*`]: v }), + {} + ), + loaderOptions: { + path: path.join(__dirname, '../../languages'), + watch: true, + }, + }), + loader: I18nJsonLoader, + inject: [ConfigService], + resolvers: [new HeaderResolver(['x-custom-lang'])], + }), + MessageMiddlewareModule, + ], + controllers: [], +}) +export class MessageModule {} diff --git a/src/common/message/middleware/custom-language/message.custom-language.middleware.ts b/src/common/message/middleware/custom-language/message.custom-language.middleware.ts new file mode 100644 index 0000000..da59bc5 --- /dev/null +++ b/src/common/message/middleware/custom-language/message.custom-language.middleware.ts @@ -0,0 +1,51 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Response, NextFunction } from 'express'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class MessageCustomLanguageMiddleware implements NestMiddleware { + private readonly appDefaultLanguage: string[]; + + constructor( + private readonly helperArrayService: HelperArrayService, + private readonly configService: ConfigService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + let language: string = this.appDefaultLanguage.join(','); + let customLang: string[] = this.appDefaultLanguage; + + const reqLanguages: string = req.headers['x-custom-lang'] as string; + const enumLanguage: string[] = Object.values(ENUM_MESSAGE_LANGUAGE); + if (reqLanguages) { + const splitLanguage: string[] = reqLanguages + .split(',') + .map((val) => val.toLowerCase()); + const uniqueLanguage = + this.helperArrayService.unique(splitLanguage); + const languages: string[] = uniqueLanguage.filter((val) => + this.helperArrayService.includes(enumLanguage, val) + ); + + if (languages.length > 0) { + language = languages.join(','); + customLang = languages; + } + } + + req.__customLang = customLang; + req.headers['x-custom-lang'] = language; + + next(); + } +} diff --git a/src/common/message/middleware/message.middleware.module.ts b/src/common/message/middleware/message.middleware.module.ts new file mode 100644 index 0000000..7f1cd3e --- /dev/null +++ b/src/common/message/middleware/message.middleware.module.ts @@ -0,0 +1,9 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { MessageCustomLanguageMiddleware } from 'src/common/message/middleware/custom-language/message.custom-language.middleware'; + +@Module({}) +export class MessageMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(MessageCustomLanguageMiddleware).forRoutes('*'); + } +} diff --git a/src/common/message/serializations/message.language.serialization.ts b/src/common/message/serializations/message.language.serialization.ts new file mode 100644 index 0000000..18edec9 --- /dev/null +++ b/src/common/message/serializations/message.language.serialization.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; + +export class MessageLanguageSerialization { + @ApiProperty({ + enum: ENUM_MESSAGE_LANGUAGE, + type: 'array', + isArray: true, + }) + language: ENUM_MESSAGE_LANGUAGE[]; +} diff --git a/src/common/message/services/message.service.ts b/src/common/message/services/message.service.ts new file mode 100644 index 0000000..2f3952a --- /dev/null +++ b/src/common/message/services/message.service.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ValidationError } from 'class-validator'; +import { I18nService } from 'nestjs-i18n'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { + IMessage, + IMessageOptions, + IMessageSetOptions, +} from 'src/common/message/interfaces/message.interface'; +import { IMessageService } from 'src/common/message/interfaces/message.service.interface'; + +@Injectable() +export class MessageService implements IMessageService { + private readonly appDefaultLanguage: string[]; + + constructor( + private readonly i18n: I18nService, + private readonly configService: ConfigService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async getAvailableLanguages(): Promise { + return Object.values(ENUM_MESSAGE_LANGUAGE); + } + + setMessage( + lang: string, + key: string, + options?: IMessageSetOptions + ): T { + return this.i18n.translate(key, { + lang: lang ?? this.appDefaultLanguage.join(','), + args: options?.properties, + }) as T; + } + + async getRequestErrorsMessage( + requestErrors: ValidationError[], + customLanguages?: string[] + ): Promise { + const messages: Array = []; + for (const transfomer of requestErrors) { + let children: Record[] = transfomer.children; + let constraints: string[] = Object.keys( + transfomer.constraints ?? [] + ); + const errors: IErrors[] = []; + let property: string = transfomer.property; + let propertyValue: string = transfomer.value; + + if (children.length > 0) { + while (children.length > 0) { + for (const child of children) { + property = `${property}.${child.property}`; + + if (child.children?.length > 0) { + children = child.children; + break; + } else if (child.constraints) { + constraints = Object.keys(child.constraints); + children = []; + propertyValue = child.value; + break; + } + } + } + } + + for (const constraint of constraints) { + const message = await this.get(`request.${constraint}`, { + customLanguages, + properties: { + property, + value: propertyValue, + }, + }); + errors.push({ + property, + message, + }); + } + + messages.push(errors); + } + + return messages.flat(1) as IErrors[]; + } + + async getImportErrorsMessage( + errors: IValidationErrorImport[], + customLanguages?: string[] + ): Promise { + const newErrors: IErrorsImport[] = []; + for (const error of errors) { + newErrors.push({ + row: error.row, + file: error.file, + errors: await this.getRequestErrorsMessage( + error.errors, + customLanguages + ), + }); + } + + return newErrors; + } + + async get(key: string, options?: IMessageOptions): Promise { + const properties = options?.properties; + const customLanguages = + options?.customLanguages?.length > 0 + ? options.customLanguages + : this.appDefaultLanguage; + + const messages: IMessage = {}; + for (const customLanguage of customLanguages) { + messages[customLanguage] = await this.setMessage( + customLanguage, + key, + { + properties, + } + ); + } + + if (customLanguages.length <= 1) { + return messages[customLanguages[0]] as T; + } + + return messages as T; + } +} diff --git a/src/common/pagination/constants/pagination.constant.ts b/src/common/pagination/constants/pagination.constant.ts new file mode 100644 index 0000000..ebbfdcf --- /dev/null +++ b/src/common/pagination/constants/pagination.constant.ts @@ -0,0 +1,12 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const PAGINATION_PER_PAGE = 20; +export const PAGINATION_MAX_PER_PAGE = 100; +export const PAGINATION_PAGE = 1; +export const PAGINATION_MAX_PAGE = 20; +export const PAGINATION_ORDER_BY = 'createdAt'; +export const PAGINATION_ORDER_DIRECTION: ENUM_PAGINATION_ORDER_DIRECTION_TYPE = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const PAGINATION_AVAILABLE_ORDER_BY: string[] = ['createdAt']; +export const PAGINATION_AVAILABLE_ORDER_DIRECTION: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] = + Object.values(ENUM_PAGINATION_ORDER_DIRECTION_TYPE); diff --git a/src/common/pagination/constants/pagination.enum.constant.ts b/src/common/pagination/constants/pagination.enum.constant.ts new file mode 100644 index 0000000..9e3b3cb --- /dev/null +++ b/src/common/pagination/constants/pagination.enum.constant.ts @@ -0,0 +1,14 @@ +export enum ENUM_PAGINATION_ORDER_DIRECTION_TYPE { + ASC = 'asc', + DESC = 'desc', +} + +export enum ENUM_PAGINATION_FILTER_CASE_OPTIONS { + UPPERCASE = 'UPPERCASE', + LOWERCASE = 'LOWERCASE', +} + +export enum ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS { + START_OF_DAY = 'START_OF_DAY', + END_OF_DAY = 'END_OF_DAY', +} diff --git a/src/common/pagination/decorators/pagination.decorator.ts b/src/common/pagination/decorators/pagination.decorator.ts new file mode 100644 index 0000000..29449b5 --- /dev/null +++ b/src/common/pagination/decorators/pagination.decorator.ts @@ -0,0 +1,85 @@ +import { Query } from '@nestjs/common'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { + IPaginationFilterDateOptions, + IPaginationFilterStringContainOptions, + IPaginationFilterStringEqualOptions, +} from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationFilterContainPipe } from 'src/common/pagination/pipes/pagination.filter-contain.pipe'; +import { PaginationFilterDatePipe } from 'src/common/pagination/pipes/pagination.filter-date.pipe'; +import { PaginationFilterEqualObjectIdPipe } from 'src/common/pagination/pipes/pagination.filter-equal-object-id.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 { 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'; + +export function PaginationQuery( + defaultPerPage: number, + defaultOrderBy: string, + defaultOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableSearch: string[], + availableOrderBy: string[] +): ParameterDecorator { + return Query( + PaginationSearchPipe(availableSearch), + PaginationPagingPipe(defaultPerPage), + PaginationOrderPipe( + defaultOrderBy, + defaultOrderDirection, + availableOrderBy + ) + ); +} + +export function PaginationQuerySearch( + availableSearch: string[] +): ParameterDecorator { + return Query(PaginationSearchPipe(availableSearch)); +} + +export function PaginationQueryFilterInBoolean( + field: string, + defaultValue: boolean[] +): ParameterDecorator { + return Query(field, PaginationFilterInBooleanPipe(defaultValue)); +} + +export function PaginationQueryFilterInEnum( + field: string, + defaultValue: T, + defaultEnum: Record +): ParameterDecorator { + return Query( + field, + PaginationFilterInEnumPipe(defaultValue, defaultEnum) + ); +} + +export function PaginationQueryFilterEqual( + field: string, + options?: IPaginationFilterStringEqualOptions +): ParameterDecorator { + return Query(field, PaginationFilterEqualPipe(options)); +} + +export function PaginationQueryFilterContain( + field: string, + options?: IPaginationFilterStringContainOptions +): ParameterDecorator { + return Query(field, PaginationFilterContainPipe(options)); +} + +export function PaginationQueryFilterDate( + field: string, + options?: IPaginationFilterDateOptions +): ParameterDecorator { + return Query(field, PaginationFilterDatePipe(options)); +} + +export function PaginationQueryFilterEqualObjectId( + field: string +): ParameterDecorator { + return Query(field, PaginationFilterEqualObjectIdPipe); +} diff --git a/src/common/pagination/dtos/pagination.list.dto.ts b/src/common/pagination/dtos/pagination.list.dto.ts new file mode 100644 index 0000000..7225b31 --- /dev/null +++ b/src/common/pagination/dtos/pagination.list.dto.ts @@ -0,0 +1,23 @@ +import { ApiHideProperty } from '@nestjs/swagger'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; + +export class PaginationListDto { + @ApiHideProperty() + _search: Record; + + @ApiHideProperty() + _limit: number; + + @ApiHideProperty() + _offset: number; + + @ApiHideProperty() + _order: IPaginationOrder; + + @ApiHideProperty() + _availableOrderBy: string[]; + + @ApiHideProperty() + _availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[]; +} diff --git a/src/common/pagination/interfaces/pagination.interface.ts b/src/common/pagination/interfaces/pagination.interface.ts new file mode 100644 index 0000000..a7cc8c0 --- /dev/null +++ b/src/common/pagination/interfaces/pagination.interface.ts @@ -0,0 +1,35 @@ +import { + ENUM_PAGINATION_FILTER_CASE_OPTIONS, + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE, +} from 'src/common/pagination/constants/pagination.enum.constant'; + +export type IPaginationOrder = Record< + string, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE +>; + +export interface IPaginationPaging { + limit: number; + offset: number; +} + +export interface IPaginationOptions { + paging?: IPaginationPaging; + order?: IPaginationOrder; +} + +export interface IPaginationFilterDateOptions { + time?: ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS; +} + +export interface IPaginationFilterStringContainOptions { + case?: ENUM_PAGINATION_FILTER_CASE_OPTIONS; + trim?: boolean; + fullMatch?: boolean; +} + +export interface IPaginationFilterStringEqualOptions + extends IPaginationFilterStringContainOptions { + isNumber?: boolean; +} diff --git a/src/common/pagination/interfaces/pagination.service.interface.ts b/src/common/pagination/interfaces/pagination.service.interface.ts new file mode 100644 index 0000000..f0ee902 --- /dev/null +++ b/src/common/pagination/interfaces/pagination.service.interface.ts @@ -0,0 +1,43 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; + +export interface IPaginationService { + offset(page: number, perPage: number): number; + + totalPage(totalData: number, perPage: number): number; + + offsetWithoutMax(page: number, perPage: number): number; + + totalPageWithoutMax(totalData: number, perPage: number): number; + + page(page: number): number; + + perPage(perPage: number): number; + + order( + orderByValue: string, + orderDirectionValue: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[], + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] + ): IPaginationOrder; + + search( + searchValue: string, + availableSearch: string[] + ): Record | undefined; + + filterEqual( + field: string, + filterValue?: T + ): Record | undefined; + + filterContain( + field: string, + filterValue?: string + ): Record | undefined; + + filterIn( + field: string, + filterValue?: T[] + ): Record | undefined; +} diff --git a/src/common/pagination/pagination.module.ts b/src/common/pagination/pagination.module.ts new file mode 100644 index 0000000..ec702e2 --- /dev/null +++ b/src/common/pagination/pagination.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { PaginationService } from './services/pagination.service'; + +@Global() +@Module({ + providers: [PaginationService], + exports: [PaginationService], + imports: [], +}) +export class PaginationModule {} diff --git a/src/common/pagination/pipes/pagination.filter-contain.pipe.ts b/src/common/pagination/pipes/pagination.filter-contain.pipe.ts new file mode 100644 index 0000000..3ce8ffc --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-contain.pipe.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { ENUM_PAGINATION_FILTER_CASE_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationFilterStringContainOptions } 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'; + +export function PaginationFilterContainPipe( + options?: IPaginationFilterStringContainOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterContainPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + value = ''; + } + + if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.UPPERCASE + ) { + value = value.toUpperCase(); + } else if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.LOWERCASE + ) { + value = value.toUpperCase(); + } + + if (options?.trim) { + value = value.trim(); + } + + if (options?.fullMatch) { + return this.paginationService.filterContainFullMatch( + field, + value + ); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterContain(field, value); + } + } + + return mixin(MixinPaginationFilterContainPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-date.pipe.ts b/src/common/pagination/pipes/pagination.filter-date.pipe.ts new file mode 100644 index 0000000..71dadf3 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-date.pipe.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + 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 { 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'; + +export function PaginationFilterDatePipe( + options?: IPaginationFilterDateOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterDatePipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperDateService: HelperDateService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let date: Date = this.helperDateService.create(value); + + if ( + options?.time === + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.END_OF_DAY + ) { + date = this.helperDateService.endOfDay(date); + } else if ( + options?.time === + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.START_OF_DAY + ) { + date = this.helperDateService.startOfDay(date); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterDate(field, date); + } + } + + return mixin(MixinPaginationFilterDatePipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts b/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts new file mode 100644 index 0000000..55073fa --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { Types } from 'mongoose'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable({ scope: Scope.REQUEST }) +export class PaginationFilterEqualObjectIdPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + return undefined; + } + + value = value.trim(); + const finalValue = Types.ObjectId.isValid(value) + ? new Types.ObjectId(value) + : value; + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterEqual( + field, + finalValue + ); + } +} diff --git a/src/common/pagination/pipes/pagination.filter-equal.pipe.ts b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts new file mode 100644 index 0000000..84d8b22 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_PAGINATION_FILTER_CASE_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationFilterStringEqualOptions } 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'; + +export function PaginationFilterEqualPipe( + options?: IPaginationFilterStringEqualOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterEqualPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperNumberService: HelperNumberService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + return undefined; + } + + if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.UPPERCASE + ) { + value = value.toUpperCase(); + } else if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.LOWERCASE + ) { + value = value.toUpperCase(); + } + + if (options?.trim) { + value = value.trim(); + } + + let finalValue: string | number = value; + if (options?.isNumber) { + finalValue = this.helperNumberService.check(value) + ? this.helperNumberService.create(value) + : value; + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue, + }; + + return this.paginationService.filterEqual( + field, + finalValue + ); + } + } + + return mixin(MixinPaginationFilterEqualPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts new file mode 100644 index 0000000..3665e73 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterInBooleanPipe( + defaultValue: boolean[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterInBooleanPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperArrayService: HelperArrayService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let finalValue: boolean[] = defaultValue as boolean[]; + + if (value) { + finalValue = this.helperArrayService.unique( + value.split(',').map((val: string) => val === 'true') + ); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue, + }; + + return this.paginationService.filterIn(field, finalValue); + } + } + + return mixin(MixinPaginationFilterInBooleanPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts new file mode 100644 index 0000000..e7b8a1c --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterInEnumPipe( + defaultValue: T, + defaultEnum: Record +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterInEnumPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let finalValue: T[] = defaultValue as T[]; + + if (value) { + finalValue = value + .split(',') + .map((val: string) => defaultEnum[val]) + .filter((val: string) => val) as T[]; + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue as string[], + }; + + return this.paginationService.filterIn(field, finalValue); + } + } + + return mixin(MixinPaginationFilterInEnumPipe); +} diff --git a/src/common/pagination/pipes/pagination.order.pipe.ts b/src/common/pagination/pipes/pagination.order.pipe.ts new file mode 100644 index 0000000..81c1fc5 --- /dev/null +++ b/src/common/pagination/pipes/pagination.order.pipe.ts @@ -0,0 +1,54 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PAGINATION_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 { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationOrderPipe( + defaultOrderBy: string, + defaultOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationOrderPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: Record + ): Promise> { + const orderBy: string = value?.orderBy ?? defaultOrderBy; + const orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE = + value?.orderDirection ?? defaultOrderDirection; + const availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] = + PAGINATION_AVAILABLE_ORDER_DIRECTION; + + const order: Record = this.paginationService.order( + orderBy, + orderDirection, + availableOrderBy + ); + + this.request.__pagination = { + ...this.request.__pagination, + orderBy, + orderDirection, + availableOrderBy, + availableOrderDirection, + }; + + return { + ...value, + _order: order, + _availableOrderBy: availableOrderBy, + _availableOrderDirection: availableOrderDirection, + }; + } + } + + return mixin(MixinPaginationOrderPipe); +} diff --git a/src/common/pagination/pipes/pagination.paging.pipe.ts b/src/common/pagination/pipes/pagination.paging.pipe.ts new file mode 100644 index 0000000..eece330 --- /dev/null +++ b/src/common/pagination/pipes/pagination.paging.pipe.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationPagingPipe( + defaultPerPage: number +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationPagingPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperNumberService: HelperNumberService + ) {} + + async transform( + value: Record + ): Promise> { + const page: number = this.paginationService.page( + this.helperNumberService.create(value?.page ?? 1) + ); + const perPage: number = this.paginationService.perPage( + this.helperNumberService.create( + value?.perPage ?? defaultPerPage + ) + ); + const offset: number = this.paginationService.offset(page, perPage); + + this.request.__pagination = { + ...this.request.__pagination, + page, + perPage, + }; + + return { + ...value, + page, + perPage, + _limit: perPage, + _offset: offset, + }; + } + } + + return mixin(MixinPaginationPagingPipe); +} diff --git a/src/common/pagination/pipes/pagination.search.pipe.ts b/src/common/pagination/pipes/pagination.search.pipe.ts new file mode 100644 index 0000000..67c7314 --- /dev/null +++ b/src/common/pagination/pipes/pagination.search.pipe.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationSearchPipe( + availableSearch: string[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationSearchPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: Record + ): Promise> { + const searchText = value?.search ?? ''; + const search: Record = this.paginationService.search( + value?.search, + availableSearch + ); + + this.request.__pagination = { + ...this.request.__pagination, + search: searchText, + availableSearch, + }; + + return { + ...value, + _search: search, + _availableSearch: availableSearch, + }; + } + } + + return mixin(MixinPaginationSearchPipe); +} diff --git a/src/common/pagination/services/pagination.service.ts b/src/common/pagination/services/pagination.service.ts new file mode 100644 index 0000000..c6f7a1a --- /dev/null +++ b/src/common/pagination/services/pagination.service.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@nestjs/common'; +import { + PAGINATION_AVAILABLE_ORDER_BY, + PAGINATION_MAX_PAGE, + PAGINATION_MAX_PER_PAGE, + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_PAGE, + PAGINATION_PER_PAGE, +} from 'src/common/pagination/constants/pagination.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; +import { IPaginationService } from 'src/common/pagination/interfaces/pagination.service.interface'; + +@Injectable() +export class PaginationService implements IPaginationService { + offset(page: number, perPage: number): number { + page = page > PAGINATION_MAX_PAGE ? PAGINATION_MAX_PAGE : page; + perPage = + perPage > PAGINATION_MAX_PER_PAGE + ? PAGINATION_MAX_PER_PAGE + : perPage; + const offset: number = (page - 1) * perPage; + + return offset; + } + + totalPage(totalData: number, perPage: number): number { + let totalPage = Math.ceil(totalData / perPage); + totalPage = totalPage === 0 ? 1 : totalPage; + return totalPage > PAGINATION_MAX_PAGE + ? PAGINATION_MAX_PAGE + : totalPage; + } + + offsetWithoutMax(page: number, perPage: number): number { + const offset: number = (page - 1) * perPage; + return offset; + } + + totalPageWithoutMax(totalData: number, perPage: number): number { + let totalPage = Math.ceil(totalData / perPage); + totalPage = totalPage === 0 ? 1 : totalPage; + return totalPage; + } + + page(page?: number): number { + return page + ? page > PAGINATION_MAX_PAGE + ? PAGINATION_MAX_PAGE + : page + : PAGINATION_PAGE; + } + + perPage(perPage?: number): number { + return perPage + ? perPage > PAGINATION_MAX_PER_PAGE + ? PAGINATION_MAX_PER_PAGE + : perPage + : PAGINATION_PER_PAGE; + } + + order( + orderByValue = PAGINATION_ORDER_BY, + orderDirectionValue = PAGINATION_ORDER_DIRECTION, + availableOrderBy = PAGINATION_AVAILABLE_ORDER_BY + ): IPaginationOrder { + const orderBy: string = availableOrderBy.includes(orderByValue) + ? orderByValue + : PAGINATION_ORDER_BY; + + return { [orderBy]: orderDirectionValue }; + } + + search( + searchValue = '', + availableSearch: string[] + ): Record | undefined { + if (!searchValue) { + return undefined; + } + + return { + $or: availableSearch.map((val) => ({ + [val]: { + $regex: new RegExp(searchValue), + $options: 'i', + }, + })), + }; + } + + filterEqual(field: string, filterValue: T): Record { + return { [field]: filterValue }; + } + + filterContain( + field: string, + filterValue: string + ): Record { + return { + [field]: { + $regex: new RegExp(filterValue), + $options: 'i', + }, + }; + } + + filterContainFullMatch( + field: string, + filterValue: string + ): Record { + return { + [field]: { + $regex: new RegExp(`\\b${filterValue}\\b`), + $options: 'i', + }, + }; + } + + filterIn( + field: string, + filterValue: T[] + ): Record { + return { + [field]: { + $in: filterValue, + }, + }; + } + + filterDate(field: string, filterValue: Date): Record { + return { + [field]: filterValue, + }; + } +} diff --git a/src/common/request/constants/request.constant.ts b/src/common/request/constants/request.constant.ts new file mode 100644 index 0000000..bf14ba4 --- /dev/null +++ b/src/common/request/constants/request.constant.ts @@ -0,0 +1,5 @@ +export const REQUEST_PARAM_CLASS_DTOS_META_KEY = 'RequestParamClassDtosMetaKey'; + +export const REQUEST_CUSTOM_TIMEOUT_META_KEY = 'RequestCustomTimeoutMetaKey'; +export const REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY = + 'RequestCustomTimeoutValueMetaKey'; diff --git a/src/common/request/constants/request.enum.constant.ts b/src/common/request/constants/request.enum.constant.ts new file mode 100644 index 0000000..52a5457 --- /dev/null +++ b/src/common/request/constants/request.enum.constant.ts @@ -0,0 +1,9 @@ +export enum ENUM_REQUEST_METHOD { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', + OPTIONS = 'OPTIONS', + HEAD = 'HEAD', +} diff --git a/src/common/request/constants/request.status-code.constant.ts b/src/common/request/constants/request.status-code.constant.ts new file mode 100644 index 0000000..f19d64e --- /dev/null +++ b/src/common/request/constants/request.status-code.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_REQUEST_STATUS_CODE_ERROR { + REQUEST_VALIDATION_ERROR = 5981, + REQUEST_TIMESTAMP_INVALID_ERROR = 5982, + REQUEST_USER_AGENT_INVALID_ERROR = 5983, + REQUEST_USER_AGENT_OS_INVALID_ERROR = 5984, + REQUEST_USER_AGENT_BROWSER_INVALID_ERROR = 5985, +} diff --git a/src/common/request/decorators/request.decorator.ts b/src/common/request/decorators/request.decorator.ts new file mode 100644 index 0000000..4be89bb --- /dev/null +++ b/src/common/request/decorators/request.decorator.ts @@ -0,0 +1,78 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { ClassConstructor } from 'class-transformer'; +import { + REQUEST_CUSTOM_TIMEOUT_META_KEY, + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, + REQUEST_PARAM_CLASS_DTOS_META_KEY, +} from 'src/common/request/constants/request.constant'; +import { RequestParamRawGuard } from 'src/common/request/guards/request.param.guard'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { IResult } from 'ua-parser-js'; +import { RequestTimestampInterceptor } from 'src/common/request/interceptors/request.timestamp.interceptor'; +import { RequestUserAgentInterceptor } from 'src/common/request/interceptors/request.user-agent.interceptor'; + +export const RequestUserAgent: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): IResult => { + const { __userAgent } = ctx.switchToHttp().getRequest() as IRequestApp; + return __userAgent; + } +); + +export const RequestId: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): string => { + const { __id } = ctx.switchToHttp().getRequest() as IRequestApp; + return __id; + } +); + +export const RequestXTimestamp: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const { __xTimestamp } = ctx.switchToHttp().getRequest() as IRequestApp; + return __xTimestamp; + } +); + +export const RequestTimestamp: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const { __timestamp } = ctx.switchToHttp().getRequest() as IRequestApp; + return __timestamp; + } +); + +export const RequestCustomLang: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): string[] => { + const { __customLang } = ctx.switchToHttp().getRequest() as IRequestApp; + return __customLang; + } +); + +export function RequestParamGuard( + ...classValidation: ClassConstructor[] +): MethodDecorator { + return applyDecorators( + UseGuards(RequestParamRawGuard), + SetMetadata(REQUEST_PARAM_CLASS_DTOS_META_KEY, classValidation) + ); +} + +export function RequestValidateUserAgent(): MethodDecorator { + return applyDecorators(UseInterceptors(RequestUserAgentInterceptor)); +} + +export function RequestValidateTimestamp(): MethodDecorator { + return applyDecorators(UseInterceptors(RequestTimestampInterceptor)); +} + +export function RequestTimeout(seconds: string): MethodDecorator { + return applyDecorators( + SetMetadata(REQUEST_CUSTOM_TIMEOUT_META_KEY, true), + SetMetadata(REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, seconds) + ); +} diff --git a/src/common/request/guards/request.param.guard.ts b/src/common/request/guards/request.param.guard.ts new file mode 100644 index 0000000..df7afa7 --- /dev/null +++ b/src/common/request/guards/request.param.guard.ts @@ -0,0 +1,40 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; +import { REQUEST_PARAM_CLASS_DTOS_META_KEY } from 'src/common/request/constants/request.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; + +@Injectable() +export class RequestParamRawGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const { params } = context.switchToHttp().getRequest(); + const classDtos: ClassConstructor[] = this.reflector.get< + ClassConstructor[] + >(REQUEST_PARAM_CLASS_DTOS_META_KEY, context.getHandler()); + + for (const clsDto of classDtos) { + const request = plainToInstance(clsDto, params); + + const errors: ValidationError[] = await validate(request); + + if (errors.length > 0) { + throw new BadRequestException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + message: 'http.clientError.badRequest', + errors: errors, + }); + } + } + + return true; + } +} diff --git a/src/common/request/interceptors/request.timeout.interceptor.ts b/src/common/request/interceptors/request.timeout.interceptor.ts new file mode 100644 index 0000000..84ceaf4 --- /dev/null +++ b/src/common/request/interceptors/request.timeout.interceptor.ts @@ -0,0 +1,81 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + RequestTimeoutException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; +import ms from 'ms'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + REQUEST_CUSTOM_TIMEOUT_META_KEY, + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, +} from 'src/common/request/constants/request.constant'; + +@Injectable() +export class RequestTimeoutInterceptor + implements NestInterceptor> +{ + private readonly maxTimeoutInSecond: number; + + constructor( + private readonly configService: ConfigService, + private readonly reflector: Reflector + ) { + this.maxTimeoutInSecond = + this.configService.get('request.timeout'); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const customTimeout = this.reflector.get( + REQUEST_CUSTOM_TIMEOUT_META_KEY, + context.getHandler() + ); + + if (customTimeout) { + const seconds: string = this.reflector.get( + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, + context.getHandler() + ); + + return next.handle().pipe( + timeout(ms(seconds)), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + message: 'http.clientError.requestTimeOut', + }); + } + return throwError(() => err); + }) + ); + } else { + return next.handle().pipe( + timeout(this.maxTimeoutInSecond), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + message: 'http.clientError.requestTimeOut', + }); + } + return throwError(() => err); + }) + ); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interceptors/request.timestamp.interceptor.ts b/src/common/request/interceptors/request.timestamp.interceptor.ts new file mode 100644 index 0000000..2875731 --- /dev/null +++ b/src/common/request/interceptors/request.timestamp.interceptor.ts @@ -0,0 +1,76 @@ +import { + CallHandler, + ExecutionContext, + ForbiddenException, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Observable } from 'rxjs'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimestampInterceptor + implements NestInterceptor> +{ + private readonly maxRequestTimestampInMs: number; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.maxRequestTimestampInMs = this.configService.get( + 'request.timestamp.toleranceTimeInMs' + ); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const request: IRequestApp = context.switchToHttp().getRequest(); + const timestamp: number = request.__timestamp; + + if (!timestamp) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'auth.apiKey.error.timestampInvalid', + }); + } + + const checkTimestamp = + this.helperDateService.checkTimestamp(timestamp); + + if (!timestamp || !checkTimestamp) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'request.error.timestampInvalid', + }); + } + + const timestampDate = this.helperDateService.create(timestamp); + + const toleranceMin = this.helperDateService.backwardInMilliseconds( + this.maxRequestTimestampInMs + ); + const toleranceMax = this.helperDateService.forwardInMilliseconds( + this.maxRequestTimestampInMs + ); + + if (timestampDate < toleranceMin || timestampDate > toleranceMax) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'request.error.timestampInvalid', + }); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interceptors/request.user-agent.interceptor.ts b/src/common/request/interceptors/request.user-agent.interceptor.ts new file mode 100644 index 0000000..3ee848f --- /dev/null +++ b/src/common/request/interceptors/request.user-agent.interceptor.ts @@ -0,0 +1,68 @@ +import { + CallHandler, + ExecutionContext, + ForbiddenException, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Observable } from 'rxjs'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { IResult } from 'ua-parser-js'; + +@Injectable() +export class RequestUserAgentInterceptor + implements NestInterceptor> +{ + private readonly userAgentOs: string[]; + private readonly userAgentBrowser: string[]; + + constructor(private readonly configService: ConfigService) { + this.userAgentBrowser = this.configService.get( + 'request.userAgent.browser' + ); + this.userAgentOs = this.configService.get( + 'request.userAgent.os' + ); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const request: IRequestApp = context + .switchToHttp() + .getRequest(); + + const userAgent: IResult = request.__userAgent; + + if ( + !this.userAgentOs.some((val) => + val.match(new RegExp(userAgent.os.name)) + ) + ) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + message: 'request.error.userAgentOsInvalid', + }); + } + + if ( + !this.userAgentBrowser.some((val) => + val.match(new RegExp(userAgent.browser.name)) + ) + ) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + message: 'request.error.userAgentBrowserInvalid', + }); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interfaces/request.interface.ts b/src/common/request/interfaces/request.interface.ts new file mode 100644 index 0000000..e859dee --- /dev/null +++ b/src/common/request/interfaces/request.interface.ts @@ -0,0 +1,27 @@ +import { Request } from 'express'; +import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface'; +import { RequestPaginationSerialization } from 'src/common/request/serializations/request.pagination.serialization'; +import { IResult } from 'ua-parser-js'; + +export interface IRequestApp extends Request { + apiKey?: IApiKeyPayload; + user?: Record; + + __id: string; + __xTimestamp?: number; + __timestamp: number; + __timezone: string; + __customLang: string[]; + __version: string; + __repoVersion: string; + __userAgent: IResult; + + __class?: string; + __function?: string; + + __filters?: Record< + string, + string | number | boolean | Array + >; + __pagination?: RequestPaginationSerialization; +} diff --git a/src/common/request/middleware/body-parser/request.body-parser.middleware.ts b/src/common/request/middleware/body-parser/request.body-parser.middleware.ts new file mode 100644 index 0000000..fe9ffd2 --- /dev/null +++ b/src/common/request/middleware/body-parser/request.body-parser.middleware.ts @@ -0,0 +1,73 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import bodyParser from 'body-parser'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class RequestUrlencodedBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.urlencoded.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.urlencoded({ + extended: false, + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestJsonBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.json.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.json({ + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestRawBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.raw.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.raw({ + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestTextBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.text.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.text({ + limit: this.maxFile, + })(req, res, next); + } +} diff --git a/src/common/request/middleware/cors/request.cors.middleware.ts b/src/common/request/middleware/cors/request.cors.middleware.ts new file mode 100644 index 0000000..4195bea --- /dev/null +++ b/src/common/request/middleware/cors/request.cors.middleware.ts @@ -0,0 +1,44 @@ +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'; + +@Injectable() +export class RequestCorsMiddleware implements NestMiddleware { + private readonly appEnv: ENUM_APP_ENVIRONMENT; + private readonly allowOrigin: string | boolean | string[]; + private readonly allowMethod: string[]; + private readonly allowHeader: string[]; + + constructor(private readonly configService: ConfigService) { + this.appEnv = this.configService.get('app.env'); + this.allowOrigin = this.configService.get( + 'request.cors.allowOrigin' + ); + this.allowMethod = this.configService.get( + 'request.cors.allowMethod' + ); + this.allowHeader = this.configService.get( + 'request.cors.allowHeader' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + const allowOrigin = + this.appEnv === ENUM_APP_ENVIRONMENT.PRODUCTION + ? this.allowOrigin + : '*'; + + const corsOptions: CorsOptions = { + origin: allowOrigin, + methods: this.allowMethod, + allowedHeaders: this.allowHeader, + preflightContinue: false, + credentials: true, + optionsSuccessStatus: HttpStatus.NO_CONTENT, + }; + + cors(corsOptions)(req, res, next); + } +} diff --git a/src/common/request/middleware/helmet/request.helmet.middleware.ts b/src/common/request/middleware/helmet/request.helmet.middleware.ts new file mode 100644 index 0000000..b526e01 --- /dev/null +++ b/src/common/request/middleware/helmet/request.helmet.middleware.ts @@ -0,0 +1,10 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import helmet from 'helmet'; + +@Injectable() +export class RequestHelmetMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction): void { + helmet()(req, res, next); + } +} diff --git a/src/common/request/middleware/id/request.id.middleware.ts b/src/common/request/middleware/id/request.id.middleware.ts new file mode 100644 index 0000000..df6a6e1 --- /dev/null +++ b/src/common/request/middleware/id/request.id.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestIdMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const uuid: string = DatabaseDefaultUUID(); + + req.__id = uuid; + next(); + } +} diff --git a/src/common/request/middleware/request.middleware.module.ts b/src/common/request/middleware/request.middleware.module.ts new file mode 100644 index 0000000..a82f18a --- /dev/null +++ b/src/common/request/middleware/request.middleware.module.ts @@ -0,0 +1,36 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { + RequestJsonBodyParserMiddleware, + RequestRawBodyParserMiddleware, + RequestTextBodyParserMiddleware, + RequestUrlencodedBodyParserMiddleware, +} from 'src/common/request/middleware/body-parser/request.body-parser.middleware'; +import { RequestCorsMiddleware } from 'src/common/request/middleware/cors/request.cors.middleware'; +import { RequestHelmetMiddleware } from 'src/common/request/middleware/helmet/request.helmet.middleware'; +import { RequestIdMiddleware } from 'src/common/request/middleware/id/request.id.middleware'; +import { RequestTimestampMiddleware } from 'src/common/request/middleware/timestamp/request.timestamp.middleware'; +import { RequestTimezoneMiddleware } from 'src/common/request/middleware/timezone/request.timezone.middleware'; +import { RequestUserAgentMiddleware } from 'src/common/request/middleware/user-agent/request.user-agent.middleware'; + +import { RequestVersionMiddleware } from 'src/common/request/middleware/version/request.version.middleware'; + +@Module({}) +export class RequestMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply( + RequestHelmetMiddleware, + RequestIdMiddleware, + RequestJsonBodyParserMiddleware, + RequestTextBodyParserMiddleware, + RequestRawBodyParserMiddleware, + RequestUrlencodedBodyParserMiddleware, + RequestCorsMiddleware, + RequestVersionMiddleware, + RequestUserAgentMiddleware, + RequestTimestampMiddleware, + RequestTimezoneMiddleware + ) + .forRoutes('*'); + } +} diff --git a/src/common/request/middleware/timestamp/request.timestamp.middleware.ts b/src/common/request/middleware/timestamp/request.timestamp.middleware.ts new file mode 100644 index 0000000..7b6be30 --- /dev/null +++ b/src/common/request/middleware/timestamp/request.timestamp.middleware.ts @@ -0,0 +1,25 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimestampMiddleware implements NestMiddleware { + constructor( + private readonly helperNumberService: HelperNumberService, + private readonly helperDateService: HelperDateService + ) {} + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + req.__xTimestamp = req['x-timestamp'] + ? this.helperNumberService.create(req['x-timestamp']) + : undefined; + req.__timestamp = this.helperDateService.timestamp(); + next(); + } +} diff --git a/src/common/request/middleware/timezone/request.timezone.middleware.ts b/src/common/request/middleware/timezone/request.timezone.middleware.ts new file mode 100644 index 0000000..408fb56 --- /dev/null +++ b/src/common/request/middleware/timezone/request.timezone.middleware.ts @@ -0,0 +1,15 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimezoneMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + req.__timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + next(); + } +} diff --git a/src/common/request/middleware/user-agent/request.user-agent.middleware.ts b/src/common/request/middleware/user-agent/request.user-agent.middleware.ts new file mode 100644 index 0000000..4bce93b --- /dev/null +++ b/src/common/request/middleware/user-agent/request.user-agent.middleware.ts @@ -0,0 +1,19 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { UAParser, IResult } from 'ua-parser-js'; + +@Injectable() +export class RequestUserAgentMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const parserUserAgent = new UAParser(req['User-Agent']); + const userAgent: IResult = parserUserAgent.getResult(); + + req.__userAgent = userAgent; + next(); + } +} diff --git a/src/common/request/middleware/version/request.version.middleware.ts b/src/common/request/middleware/version/request.version.middleware.ts new file mode 100644 index 0000000..ceee04c --- /dev/null +++ b/src/common/request/middleware/version/request.version.middleware.ts @@ -0,0 +1,53 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestVersionMiddleware implements NestMiddleware { + private readonly versioningEnable: boolean; + + private readonly versioningGlobalPrefix: string; + private readonly versioningPrefix: string; + private readonly versioningVersion: string; + + private readonly repoVersion: string; + + constructor(private readonly configService: ConfigService) { + this.versioningGlobalPrefix = + this.configService.get('app.globalPrefix'); + this.versioningEnable = this.configService.get( + 'app.versioning.enable' + ); + this.versioningPrefix = this.configService.get( + 'app.versioning.prefix' + ); + this.versioningVersion = this.configService.get( + 'app.versioning.version' + ); + this.repoVersion = this.configService.get('app.repoVersion'); + } + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const originalUrl: string = req.originalUrl; + let version = this.versioningVersion; + if ( + this.versioningEnable && + originalUrl.startsWith( + `${this.versioningGlobalPrefix}/${this.versioningPrefix}` + ) + ) { + const url: string[] = originalUrl.split('/'); + version = url[2].replace(this.versioningPrefix, ''); + } + + req.__version = version; + req.__repoVersion = this.repoVersion; + + next(); + } +} diff --git a/src/common/request/request.module.ts b/src/common/request/request.module.ts new file mode 100644 index 0000000..6258684 --- /dev/null +++ b/src/common/request/request.module.ts @@ -0,0 +1,88 @@ +import { + HttpStatus, + Module, + UnprocessableEntityException, + ValidationError, + ValidationPipe, +} from '@nestjs/common'; +import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { RequestTimeoutInterceptor } from 'src/common/request/interceptors/request.timeout.interceptor'; +import { RequestMiddlewareModule } from 'src/common/request/middleware/request.middleware.module'; +import { MaxDateTodayConstraint } from 'src/common/request/validations/request.max-date-today.validation'; +import { MinDateTodayConstraint } from 'src/common/request/validations/request.min-date-today.validation'; +import { MobileNumberAllowedConstraint } from 'src/common/request/validations/request.mobile-number-allowed.validation'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from './constants/request.status-code.constant'; +import { IsPasswordMediumConstraint } from './validations/request.is-password-medium.validation'; +import { IsPasswordStrongConstraint } from './validations/request.is-password-strong.validation'; +import { IsPasswordWeakConstraint } from './validations/request.is-password-weak.validation'; +import { IsStartWithConstraint } from './validations/request.is-start-with.validation'; +import { MaxGreaterThanEqualConstraint } from './validations/request.max-greater-than-equal.validation'; +import { MaxGreaterThanConstraint } from './validations/request.max-greater-than.validation'; +import { MinGreaterThanEqualConstraint } from './validations/request.min-greater-than-equal.validation'; +import { MinGreaterThanConstraint } from './validations/request.min-greater-than.validation'; +import { IsOnlyDigitsConstraint } from './validations/request.only-digits.validation'; +import { SafeStringConstraint } from './validations/request.safe-string.validation'; +import { SkipConstraint } from './validations/request.skip.validation'; +import { MaxBinaryFileConstraint } from 'src/common/request/validations/request.max-binary-file.validation'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: RequestTimeoutInterceptor, + }, + { + provide: APP_PIPE, + useFactory: () => + new ValidationPipe({ + transform: true, + skipNullProperties: false, + skipUndefinedProperties: false, + skipMissingProperties: false, + forbidUnknownValues: false, + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + exceptionFactory: async (errors: ValidationError[]) => + new UnprocessableEntityException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + message: 'request.validation', + errors, + }), + }), + }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + IsPasswordStrongConstraint, + IsPasswordMediumConstraint, + IsPasswordWeakConstraint, + IsStartWithConstraint, + MaxGreaterThanEqualConstraint, + MaxGreaterThanConstraint, + MinGreaterThanEqualConstraint, + MinGreaterThanConstraint, + SkipConstraint, + SafeStringConstraint, + IsOnlyDigitsConstraint, + MinDateTodayConstraint, + MobileNumberAllowedConstraint, + MaxDateTodayConstraint, + MaxBinaryFileConstraint, + ], + imports: [ + RequestMiddlewareModule, + ThrottlerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + ttl: config.get('request.throttle.ttl'), + limit: config.get('request.throttle.limit'), + }), + }), + ], +}) +export class RequestModule {} diff --git a/src/common/request/serializations/request.pagination.serialization.ts b/src/common/request/serializations/request.pagination.serialization.ts new file mode 100644 index 0000000..861afc5 --- /dev/null +++ b/src/common/request/serializations/request.pagination.serialization.ts @@ -0,0 +1,16 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export class RequestPaginationSerialization { + search: string; + filters: Record< + string, + string | number | boolean | Array + >; + page: number; + perPage: number; + orderBy: string; + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE; + availableSearch: string[]; + availableOrderBy: string[]; + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[]; +} diff --git a/src/common/request/validations/request.is-password-medium.validation.ts b/src/common/request/validations/request.is-password-medium.validation.ts new file mode 100644 index 0000000..e6fa7bd --- /dev/null +++ b/src/common/request/validations/request.is-password-medium.validation.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordMediumConstraint + implements ValidatorConstraintInterface +{ + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordMedium(value, length) + : false; + } +} + +export function IsPasswordMedium( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordMedium', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordMediumConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-password-strong.validation.ts b/src/common/request/validations/request.is-password-strong.validation.ts new file mode 100644 index 0000000..aafccfb --- /dev/null +++ b/src/common/request/validations/request.is-password-strong.validation.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordStrongConstraint + implements ValidatorConstraintInterface +{ + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordStrong(value, length) + : false; + } +} + +export function IsPasswordStrong( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordStrong', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordStrongConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-password-weak.validation.ts b/src/common/request/validations/request.is-password-weak.validation.ts new file mode 100644 index 0000000..ea605ee --- /dev/null +++ b/src/common/request/validations/request.is-password-weak.validation.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordWeakConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordMedium(value, length) + : false; + } +} + +export function IsPasswordWeak( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordWeak', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordWeakConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-start-with.validation.ts b/src/common/request/validations/request.is-start-with.validation.ts new file mode 100644 index 0000000..28c6c97 --- /dev/null +++ b/src/common/request/validations/request.is-start-with.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsStartWithConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [prefix] = args.constraints; + const check = prefix.find((val: string) => value.startsWith(val)); + return check ?? false; + } +} + +export function IsStartWith( + prefix: string[], + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsStartWith', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [prefix], + validator: IsStartWithConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-binary-file.validation.ts b/src/common/request/validations/request.max-binary-file.validation.ts new file mode 100644 index 0000000..ee9fc30 --- /dev/null +++ b/src/common/request/validations/request.max-binary-file.validation.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { ENUM_FILE_TYPE } from 'src/common/file/constants/file.enum.constant'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxBinaryFileConstraint implements ValidatorConstraintInterface { + constructor(private readonly configService: ConfigService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [type] = args.constraints; + let fileSize = 0; + + switch (type) { + case ENUM_FILE_TYPE.AUDIO: + fileSize = this.configService.get( + 'file.audio.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.EXCEL: + fileSize = this.configService.get( + 'file.excel.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.IMAGE: + fileSize = this.configService.get( + 'file.image.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.VIDEO: + fileSize = this.configService.get( + 'file.video.maxFileSize' + ); + break; + default: + break; + } + + return fileSize <= value.length; + } +} + +export function MaxBinaryFile( + type: ENUM_FILE_TYPE, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): any { + registerDecorator({ + name: 'MaxBinaryFile', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [type], + validator: MaxBinaryFileConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-date-today.validation.ts b/src/common/request/validations/request.max-date-today.validation.ts new file mode 100644 index 0000000..77def22 --- /dev/null +++ b/src/common/request/validations/request.max-date-today.validation.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxDateTodayConstraint implements ValidatorConstraintInterface { + constructor(private readonly helperDateService: HelperDateService) {} + + validate(value: string): boolean { + const todayDate = this.helperDateService.endOfDay(); + const valueDate = this.helperDateService.create(value); + return valueDate <= todayDate; + } +} + +export function MaxDateToday(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): any { + registerDecorator({ + name: 'MaxDateToday', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MaxDateTodayConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-greater-than-equal.validation.ts b/src/common/request/validations/request.max-greater-than-equal.validation.ts new file mode 100644 index 0000000..10e1515 --- /dev/null +++ b/src/common/request/validations/request.max-greater-than-equal.validation.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxGreaterThanEqualConstraint + implements ValidatorConstraintInterface +{ + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value <= relatedValue; + } +} + +export function MaxGreaterThanEqual( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MaxGreaterThanEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MaxGreaterThanEqualConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-greater-than.validation.ts b/src/common/request/validations/request.max-greater-than.validation.ts new file mode 100644 index 0000000..9a1f534 --- /dev/null +++ b/src/common/request/validations/request.max-greater-than.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxGreaterThanConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value < relatedValue; + } +} + +export function MaxGreaterThan( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MaxGreaterThan', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MaxGreaterThanConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-date-today.validation.ts b/src/common/request/validations/request.min-date-today.validation.ts new file mode 100644 index 0000000..4c7d7bf --- /dev/null +++ b/src/common/request/validations/request.min-date-today.validation.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinDateTodayConstraint implements ValidatorConstraintInterface { + constructor(private readonly helperDateService: HelperDateService) {} + + validate(value: string): boolean { + const todayDate = this.helperDateService.startOfDay(); + const valueDate = this.helperDateService.create(value); + return valueDate >= todayDate; + } +} + +export function MinDateToday(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinDateTodayEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MinDateTodayConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-greater-than-equal.validation.ts b/src/common/request/validations/request.min-greater-than-equal.validation.ts new file mode 100644 index 0000000..0fd8f4e --- /dev/null +++ b/src/common/request/validations/request.min-greater-than-equal.validation.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinGreaterThanEqualConstraint + implements ValidatorConstraintInterface +{ + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value >= relatedValue; + } +} + +export function MinGreaterThanEqual( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinGreaterThanEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MinGreaterThanEqualConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-greater-than.validation.ts b/src/common/request/validations/request.min-greater-than.validation.ts new file mode 100644 index 0000000..3a3e028 --- /dev/null +++ b/src/common/request/validations/request.min-greater-than.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinGreaterThanConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value > relatedValue; + } +} + +export function MinGreaterThan( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinGreaterThan', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MinGreaterThanConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.mobile-number-allowed.validation.ts b/src/common/request/validations/request.mobile-number-allowed.validation.ts new file mode 100644 index 0000000..b33ee4d --- /dev/null +++ b/src/common/request/validations/request.mobile-number-allowed.validation.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MobileNumberAllowedConstraint + implements ValidatorConstraintInterface +{ + constructor(private readonly settingService: SettingService) {} + + async validate(value: string): Promise { + const mobileNumbersSetting: string[] = + await this.settingService.getMobileNumberCountryCodeAllowed(); + mobileNumbersSetting; + const check = mobileNumbersSetting.find((val) => value.startsWith(val)); + + return !!check; + } +} + +export function MobileNumberAllowed(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MobileNumberAllowed', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MobileNumberAllowedConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.only-digits.validation.ts b/src/common/request/validations/request.only-digits.validation.ts new file mode 100644 index 0000000..382228f --- /dev/null +++ b/src/common/request/validations/request.only-digits.validation.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsOnlyDigitsConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperNumberService: HelperNumberService) {} + + validate(value: string): boolean { + return value ? this.helperNumberService.check(value) : false; + } +} + +export function IsOnlyDigits(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsOnlyDigits', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsOnlyDigitsConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.safe-string.validation.ts b/src/common/request/validations/request.safe-string.validation.ts new file mode 100644 index 0000000..a5ff9ab --- /dev/null +++ b/src/common/request/validations/request.safe-string.validation.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class SafeStringConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string): boolean { + return value ? this.helperStringService.checkSafeString(value) : false; + } +} + +export function SafeString(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'SafeString', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: SafeStringConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.skip.validation.ts b/src/common/request/validations/request.skip.validation.ts new file mode 100644 index 0000000..f4f2e65 --- /dev/null +++ b/src/common/request/validations/request.skip.validation.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class SkipConstraint implements ValidatorConstraintInterface { + validate(): boolean { + return true; + } +} + +export function Skip() { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'Skip', + target: object.constructor, + propertyName: propertyName, + validator: SkipConstraint, + }); + }; +} diff --git a/src/common/response/constants/response.constant.ts b/src/common/response/constants/response.constant.ts new file mode 100644 index 0000000..d5d1f2e --- /dev/null +++ b/src/common/response/constants/response.constant.ts @@ -0,0 +1,7 @@ +export const RESPONSE_SERIALIZATION_META_KEY = 'ResponseSerializationMetaKey'; +export const RESPONSE_SERIALIZATION_OPTIONS_META_KEY = + 'class_serializer:options'; +export const RESPONSE_MESSAGE_PROPERTIES_META_KEY = + 'ResponseSerializationPropertiesMetaKey'; +export const RESPONSE_MESSAGE_PATH_META_KEY = 'ResponseMessagePathMetaKey'; +export const RESPONSE_EXCEL_TYPE_META_KEY = 'ResponseExcelTypeMetaKey'; diff --git a/src/common/response/decorators/response.decorator.ts b/src/common/response/decorators/response.decorator.ts new file mode 100644 index 0000000..203b83e --- /dev/null +++ b/src/common/response/decorators/response.decorator.ts @@ -0,0 +1,79 @@ +import { + applyDecorators, + SerializeOptions, + SetMetadata, + UseInterceptors, +} from '@nestjs/common'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { + RESPONSE_EXCEL_TYPE_META_KEY, + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { ResponseDefaultInterceptor } from 'src/common/response/interceptors/response.default.interceptor'; +import { ResponseExcelInterceptor } from 'src/common/response/interceptors/response.excel.interceptor'; +import { ResponsePagingInterceptor } from 'src/common/response/interceptors/response.paging.interceptor'; +import { + IResponseOptions, + IResponsePagingOptions, + IResponseExcelOptions, +} from 'src/common/response/interfaces/response.interface'; + +export function Response( + messagePath: string, + options?: IResponseOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponseDefaultInterceptor), + SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export function ResponseExcel( + options?: IResponseExcelOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponseExcelInterceptor), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_EXCEL_TYPE_META_KEY, + options ? options.fileType : ENUM_HELPER_FILE_TYPE.CSV + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export function ResponsePaging( + messagePath: string, + options?: IResponsePagingOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponsePagingInterceptor), + SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export const ResponseSerializationOptions = SerializeOptions; diff --git a/src/common/response/interceptors/response.custom-headers.interceptor.ts b/src/common/response/interceptors/response.custom-headers.interceptor.ts new file mode 100644 index 0000000..4f39abd --- /dev/null +++ b/src/common/response/interceptors/response.custom-headers.interceptor.ts @@ -0,0 +1,41 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +// only for response success and error in controller +@Injectable() +export class ResponseCustomHeadersInterceptor + implements NestInterceptor> +{ + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const responseExpress: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + responseExpress.setHeader('x-custom-lang', request.__customLang); + responseExpress.setHeader( + 'x-timestamp', + request.__xTimestamp ?? request.__timestamp + ); + responseExpress.setHeader('x-timezone', request.__timezone); + responseExpress.setHeader('x-request-id', request.__id); + responseExpress.setHeader('x-version', request.__version); + responseExpress.setHeader('x-repo-version', request.__repoVersion); + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.default.interceptor.ts b/src/common/response/interceptors/response.default.interceptor.ts new file mode 100644 index 0000000..6976984 --- /dev/null +++ b/src/common/response/interceptors/response.default.interceptor.ts @@ -0,0 +1,147 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +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 { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { + IMessage, + IMessageOptionsProperties, +} from 'src/common/message/interfaces/message.interface'; +import { + ResponseDefaultSerialization, + ResponseMetadataSerialization, +} from 'src/common/response/serializations/response.default.serialization'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; + +@Injectable() +export class ResponseDefaultInterceptor + implements NestInterceptor> +{ + constructor( + private readonly reflector: Reflector, + private readonly messageService: MessageService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>> { + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise>) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + let messagePath: string = this.reflector.get( + RESPONSE_MESSAGE_PATH_META_KEY, + context.getHandler() + ); + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + let messageProperties: IMessageOptionsProperties = + this.reflector.get( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + context.getHandler() + ); + + // metadata + const __customLang = request.__customLang; + const __requestId = request.__id; + const __path = request.path; + const __timestamp = + request.__xTimestamp ?? request.__timestamp; + const __timezone = request.__timezone; + const __version = request.__version; + const __repoVersion = request.__repoVersion; + + // set default response + let statusCode: number = response.statusCode; + let data: Record = undefined; + let metadata: ResponseMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + + // response + const responseData = (await res) as IResponse; + + if (responseData) { + const { _metadata } = responseData; + data = responseData.data; + + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + statusCode = + _metadata?.customProperty?.statusCode ?? statusCode; + messagePath = + _metadata?.customProperty?.message ?? messagePath; + messageProperties = + _metadata?.customProperty?.messageProperties ?? + messageProperties; + + delete _metadata?.customProperty; + + metadata = { + ...metadata, + ..._metadata, + }; + } + + const message: string | IMessage = + await this.messageService.get(messagePath, { + customLanguages: __customLang, + properties: messageProperties, + }); + + return { + statusCode, + message, + _metadata: metadata, + data, + }; + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.excel.interceptor.ts b/src/common/response/interceptors/response.excel.interceptor.ts new file mode 100644 index 0000000..450d8e8 --- /dev/null +++ b/src/common/response/interceptors/response.excel.interceptor.ts @@ -0,0 +1,105 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + StreamableFile, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import { Reflector } from '@nestjs/core'; +import { IResponseExcel } from 'src/common/response/interfaces/response.interface'; +import { + RESPONSE_EXCEL_TYPE_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { WorkBook } from 'xlsx'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ResponseExcelInterceptor implements NestInterceptor> { + constructor( + private readonly reflector: Reflector, + private readonly helperFileService: HelperFileService, + private readonly helperDateService: HelperDateService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>>> { + const excelType: ENUM_HELPER_FILE_TYPE = + this.reflector.get( + RESPONSE_EXCEL_TYPE_META_KEY, + context.getHandler() + ); + + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + + // response + // set default response + const responseData = (await res) as IResponseExcel; + let data: Record[] = responseData.data; + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + // create excel + const workbook: WorkBook = + this.helperFileService.createExcelWorkbook(data); + const excel: Buffer = + this.helperFileService.writeExcelToBuffer(workbook, { + type: excelType, + }); + + // set headers + const timestamp = this.helperDateService.timestamp(); + response + .setHeader( + 'Content-Type', + ENUM_FILE_EXCEL_MIME[excelType.toUpperCase()] + ) + .setHeader( + 'Content-Disposition', + `attachment; filename=export-${timestamp}.${excelType}` + ) + .setHeader('Content-Length', excel.length); + + return new StreamableFile(excel); + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.paging.interceptor.ts b/src/common/response/interceptors/response.paging.interceptor.ts new file mode 100644 index 0000000..fc18b00 --- /dev/null +++ b/src/common/response/interceptors/response.paging.interceptor.ts @@ -0,0 +1,213 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +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 { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import qs from 'qs'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { + IMessage, + IMessageOptionsProperties, +} from 'src/common/message/interfaces/message.interface'; +import { + ResponsePagingCursorMetadataSerialization, + ResponsePagingMetadataSerialization, + ResponsePagingSerialization, +} from 'src/common/response/serializations/response.paging.serialization'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_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'; + +@Injectable() +export class ResponsePagingInterceptor + implements NestInterceptor> +{ + constructor( + private readonly reflector: Reflector, + private readonly messageService: MessageService, + private readonly helperArrayService: HelperArrayService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>> { + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + let messagePath: string = this.reflector.get( + RESPONSE_MESSAGE_PATH_META_KEY, + context.getHandler() + ); + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + let messageProperties: IMessageOptionsProperties = + this.reflector.get( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + context.getHandler() + ); + + // metadata + const __customLang = request.__customLang; + const __path = request.path; + const __requestId = request.__id; + const __timestamp = + request.__xTimestamp ?? request.__timestamp; + const __timezone = request.__timezone; + const __version = request.__version; + const __repoVersion = request.__repoVersion; + const __pagination = request.__pagination; + + let statusCode: number = response.statusCode; + let data: Record[] = []; + let metadata: ResponsePagingMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + + // response + const responseData = (await res) as IResponsePaging; + if (!responseData) { + throw new Error('Paging must have response'); + } + + const { _metadata } = responseData; + data = responseData.data; + + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + statusCode = + _metadata?.customProperty?.statusCode ?? statusCode; + messagePath = + _metadata?.customProperty?.message ?? messagePath; + messageProperties = + _metadata?.customProperty?.messageProperties ?? + messageProperties; + + 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 = __pagination.perPage; + const page: number = __pagination.page; + + const queryString = qs.stringify(query, { + encode: false, + }); + + const cursorPaginationMetadata: ResponsePagingCursorMetadataSerialization = + { + nextPage: + page < totalPage + ? `${__path}?perPage=${perPage}&page=${ + page + 1 + }&${queryString}` + : undefined, + previousPage: + page > 1 + ? `${__path}?perPage=${perPage}&page=${ + page - 1 + }&${queryString}` + : undefined, + firstPage: + totalPage > 1 + ? `${__path}?perPage=${perPage}&page=${1}&${queryString}` + : undefined, + lastPage: + totalPage > 1 + ? `${__path}?perPage=${perPage}&page=${totalPage}&${queryString}` + : undefined, + }; + + metadata = { + ...metadata, + ..._metadata, + pagination: { + ...__pagination, + ...metadata._pagination, + total, + totalPage, + }, + }; + + if ( + !this.helperArrayService.includes( + Object.values(cursorPaginationMetadata), + undefined + ) + ) { + metadata.cursor = cursorPaginationMetadata; + } + + const message: string | IMessage = + await this.messageService.get(messagePath, { + customLanguages: __customLang, + properties: messageProperties, + }); + + const responseHttp: ResponsePagingSerialization = { + statusCode, + message, + _metadata: metadata, + data, + }; + + return responseHttp; + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interfaces/response.interface.ts b/src/common/response/interfaces/response.interface.ts new file mode 100644 index 0000000..ecf8325 --- /dev/null +++ b/src/common/response/interfaces/response.interface.ts @@ -0,0 +1,50 @@ +import { ClassConstructor } from 'class-transformer'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperFileRows } from 'src/common/helper/interfaces/helper.interface'; +import { IMessageOptionsProperties } from 'src/common/message/interfaces/message.interface'; + +export interface IResponseCustomPropertyMetadata { + statusCode?: number; + message?: string; + messageProperties?: IMessageOptionsProperties; +} + +// metadata +export interface IResponseMetadata { + customProperty?: IResponseCustomPropertyMetadata; + [key: string]: any; +} + +// decorator options + +export interface IResponseOptions { + serialization?: ClassConstructor; + messageProperties?: IMessageOptionsProperties; +} + +export type IResponsePagingOptions = IResponseOptions; + +export interface IResponseExcelOptions extends IResponseOptions { + fileType?: ENUM_HELPER_FILE_TYPE; +} + +// type +export interface IResponse { + _metadata?: IResponseMetadata; + data: Record; +} + +export interface IResponsePagingPagination { + totalPage: number; + total: number; +} + +export interface IResponsePaging { + _metadata?: IResponseMetadata; + _pagination: IResponsePagingPagination; + data: Record[]; +} + +export interface IResponseExcel { + data: IHelperFileRows[]; +} diff --git a/src/common/response/middleware/response.middleware.module.ts b/src/common/response/middleware/response.middleware.module.ts new file mode 100644 index 0000000..638630b --- /dev/null +++ b/src/common/response/middleware/response.middleware.module.ts @@ -0,0 +1,9 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { ResponseTimeMiddleware } from 'src/common/response/middleware/time/response.time.middleware'; + +@Module({}) +export class ResponseMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(ResponseTimeMiddleware).forRoutes('*'); + } +} diff --git a/src/common/response/middleware/time/response.time.middleware.ts b/src/common/response/middleware/time/response.time.middleware.ts new file mode 100644 index 0000000..185f579 --- /dev/null +++ b/src/common/response/middleware/time/response.time.middleware.ts @@ -0,0 +1,10 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import responseTime from 'response-time'; + +@Injectable() +export class ResponseTimeMiddleware implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction): Promise { + responseTime()(req, res, next); + } +} diff --git a/src/common/response/response.module.ts b/src/common/response/response.module.ts new file mode 100644 index 0000000..9db6dcd --- /dev/null +++ b/src/common/response/response.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { ResponseMiddlewareModule } from 'src/common/response/middleware/response.middleware.module'; +import { ResponseCustomHeadersInterceptor } from './interceptors/response.custom-headers.interceptor'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: ResponseCustomHeadersInterceptor, + }, + ], + imports: [ResponseMiddlewareModule], +}) +export class ResponseModule {} diff --git a/src/common/response/serializations/response.default.serialization.ts b/src/common/response/serializations/response.default.serialization.ts new file mode 100644 index 0000000..d330536 --- /dev/null +++ b/src/common/response/serializations/response.default.serialization.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; + +export class ResponseMetadataSerialization { + languages: string[]; + timestamp: number; + timezone: string; + requestId: string; + path: string; + version: string; + repoVersion: string; + [key: string]: any; +} + +export class ResponseDefaultSerialization> { + @ApiProperty({ + name: 'statusCode', + type: Number, + nullable: false, + description: 'return specific status code for every endpoints', + example: 200, + }) + statusCode: number; + + @ApiProperty({ + name: 'message', + nullable: false, + description: 'Message base on language', + oneOf: [ + { + type: 'string', + example: 'message endpoint', + }, + { + type: 'object', + example: { + en: 'This is test endpoint.', + id: 'Ini adalah endpoint test', + }, + }, + ], + }) + message: string | IMessage; + + @ApiProperty({ + name: '_metadata', + nullable: true, + description: 'Contain metadata about API', + type: 'object', + required: true, + example: { + languages: ['en'], + timestamp: 1660190937231, + timezone: 'Asia/Jakarta', + requestId: '40c2f734-7247-472b-bc26-8eff6e669781', + path: '/api/v1/test/hello', + version: '1', + repoVersion: '1.0.0', + }, + }) + _metadata?: ResponseMetadataSerialization; + + data?: T; +} diff --git a/src/common/response/serializations/response.id.serialization.ts b/src/common/response/serializations/response.id.serialization.ts new file mode 100644 index 0000000..b14ec7a --- /dev/null +++ b/src/common/response/serializations/response.id.serialization.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class ResponseIdSerialization { + @ApiProperty({ + description: 'Id that representative with your target data', + example: '631d9f32a65cf07250b8938c', + required: true, + }) + @Type(() => String) + _id: string; +} diff --git a/src/common/response/serializations/response.paging.serialization.ts b/src/common/response/serializations/response.paging.serialization.ts new file mode 100644 index 0000000..9dc86cf --- /dev/null +++ b/src/common/response/serializations/response.paging.serialization.ts @@ -0,0 +1,72 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { PAGINATION_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 { RequestPaginationSerialization } from 'src/common/request/serializations/request.pagination.serialization'; +import { + ResponseDefaultSerialization, + ResponseMetadataSerialization, +} from 'src/common/response/serializations/response.default.serialization'; + +export class ResponsePagingCursorMetadataSerialization { + nextPage: string; + previousPage: string; + firstPage: string; + lastPage: string; +} + +export class ResponsePagingPaginationSerialization extends RequestPaginationSerialization { + total: number; + totalPage: number; +} + +export interface ResponsePagingMetadataSerialization + extends ResponseMetadataSerialization { + cursor?: ResponsePagingCursorMetadataSerialization; + pagination?: ResponsePagingPaginationSerialization; +} + +export class ResponsePagingSerialization< + T = Record +> extends PickType(ResponseDefaultSerialization, [ + 'statusCode', + 'message', +] as const) { + @ApiProperty({ + name: '_metadata', + nullable: false, + description: 'Contain metadata about API', + type: 'object', + required: true, + example: { + languages: ['en'], + timestamp: 1660190937231, + timezone: 'Asia/Jakarta', + requestId: '40c2f734-7247-472b-bc26-8eff6e669781', + path: '/api/v1/test/hello', + version: '1', + repoVersion: '1.0.0', + pagination: { + search: faker.name.firstName(), + page: 1, + perPage: 20, + orderBy: 'createdAt', + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + availableSearch: ['name'], + availableOrderBy: ['createdAt'], + availableOrderDirection: PAGINATION_AVAILABLE_ORDER_DIRECTION, + 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: ResponsePagingMetadataSerialization; + + readonly data: T[]; +} diff --git a/src/common/setting/constants/setting.doc.constant.ts b/src/common/setting/constants/setting.doc.constant.ts new file mode 100644 index 0000000..16108da --- /dev/null +++ b/src/common/setting/constants/setting.doc.constant.ts @@ -0,0 +1,19 @@ +export const SettingDocParamsGet = [ + { + name: 'setting', + allowEmptyValue: false, + required: true, + type: 'string', + description: 'setting id', + }, +]; + +export const SettingDocParamsGetByName = [ + { + name: 'settingName', + allowEmptyValue: false, + required: true, + type: 'string', + description: 'setting name', + }, +]; diff --git a/src/common/setting/constants/setting.enum.constant.ts b/src/common/setting/constants/setting.enum.constant.ts new file mode 100644 index 0000000..84c8a38 --- /dev/null +++ b/src/common/setting/constants/setting.enum.constant.ts @@ -0,0 +1,6 @@ +export enum ENUM_SETTING_DATA_TYPE { + BOOLEAN = 'BOOLEAN', + STRING = 'STRING', + ARRAY_OF_STRING = 'ARRAY_OF_STRING', + NUMBER = 'NUMBER', +} diff --git a/src/common/setting/constants/setting.list.constant.ts b/src/common/setting/constants/setting.list.constant.ts new file mode 100644 index 0000000..548ad7c --- /dev/null +++ b/src/common/setting/constants/setting.list.constant.ts @@ -0,0 +1,8 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const SETTING_DEFAULT_PER_PAGE = 20; +export const SETTING_DEFAULT_ORDER_BY = 'createdAt'; +export const SETTING_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const SETTING_DEFAULT_AVAILABLE_SEARCH = ['name']; +export const SETTING_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'createdAt']; diff --git a/src/common/setting/constants/setting.status-code.constant.ts b/src/common/setting/constants/setting.status-code.constant.ts new file mode 100644 index 0000000..ea07036 --- /dev/null +++ b/src/common/setting/constants/setting.status-code.constant.ts @@ -0,0 +1,4 @@ +export enum ENUM_SETTING_STATUS_CODE_ERROR { + SETTING_NOT_FOUND_ERROR = 5900, + SETTING_VALUE_NOT_ALLOWED_ERROR = 5901, +} diff --git a/src/common/setting/controllers/setting.admin.controller.ts b/src/common/setting/controllers/setting.admin.controller.ts new file mode 100644 index 0000000..0003d9e --- /dev/null +++ b/src/common/setting/controllers/setting.admin.controller.ts @@ -0,0 +1,77 @@ +import { + BadRequestException, + Body, + Controller, + InternalServerErrorException, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingUpdateGuard } from 'src/common/setting/decorators/setting.admin.decorator'; +import { GetSetting } from 'src/common/setting/decorators/setting.decorator'; +import { SettingUpdateDoc } from 'src/common/setting/docs/setting.admin.doc'; +import { SettingRequestDto } from 'src/common/setting/dtos/setting.request.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ApiTags('admin.setting') +@Controller({ + version: '1', + path: '/setting', +}) +export class SettingAdminController { + constructor(private readonly settingService: SettingService) {} + + @SettingUpdateDoc() + @Response('setting.update', { + serialization: ResponseIdSerialization, + }) + @SettingUpdateGuard() + @RequestParamGuard(SettingRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.SETTING_READ, + ENUM_AUTH_PERMISSIONS.SETTING_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:setting') + async update( + @GetSetting() setting: SettingDoc, + @Body() + body: SettingUpdateValueDto + ): Promise { + const check = await this.settingService.checkValue( + body.value, + body.type + ); + if (!check) { + throw new BadRequestException({ + statusCode: + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_VALUE_NOT_ALLOWED_ERROR, + message: 'setting.error.valueNotAllowed', + }); + } + + try { + await this.settingService.updateValue(setting, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: setting._id }, + }; + } +} diff --git a/src/common/setting/controllers/setting.controller.ts b/src/common/setting/controllers/setting.controller.ts new file mode 100644 index 0000000..1e262ac --- /dev/null +++ b/src/common/setting/controllers/setting.controller.ts @@ -0,0 +1,112 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +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 { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { + SETTING_DEFAULT_AVAILABLE_ORDER_BY, + SETTING_DEFAULT_AVAILABLE_SEARCH, + SETTING_DEFAULT_ORDER_BY, + SETTING_DEFAULT_ORDER_DIRECTION, + SETTING_DEFAULT_PER_PAGE, +} from 'src/common/setting/constants/setting.list.constant'; +import { + GetSetting, + SettingGetByNameGuard, + SettingGetGuard, +} from 'src/common/setting/decorators/setting.decorator'; +import { + SettingGetByNameDoc, + SettingGetDoc, + SettingListDoc, +} from 'src/common/setting/docs/setting.doc'; +import { SettingRequestDto } from 'src/common/setting/dtos/setting.request.dto'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingGetSerialization } from 'src/common/setting/serializations/setting.get.serialization'; +import { SettingListSerialization } from 'src/common/setting/serializations/setting.list.serialization'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ApiTags('setting') +@Controller({ + version: '1', + path: '/setting', +}) +export class SettingController { + constructor( + private readonly settingService: SettingService, + private readonly paginationService: PaginationService + ) {} + + @SettingListDoc() + @ResponsePaging('setting.list', { + serialization: SettingListSerialization, + }) + @Get('/list') + async list( + @PaginationQuery( + SETTING_DEFAULT_PER_PAGE, + SETTING_DEFAULT_ORDER_BY, + SETTING_DEFAULT_ORDER_DIRECTION, + SETTING_DEFAULT_AVAILABLE_SEARCH, + SETTING_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto + ): Promise { + const find: Record = { + ..._search, + }; + + const settings: SettingEntity[] = await this.settingService.findAll( + find, + { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + } + ); + const total: number = await this.settingService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: settings, + }; + } + + @SettingGetDoc() + @Response('setting.get', { + serialization: SettingGetSerialization, + }) + @SettingGetGuard() + @RequestParamGuard(SettingRequestDto) + @Get('get/:setting') + async get(@GetSetting(true) setting: SettingEntity): Promise { + return { data: setting }; + } + + @SettingGetByNameDoc() + @Response('setting.getByName', { + serialization: SettingGetSerialization, + }) + @SettingGetByNameGuard() + @Get('get/name/:settingName') + async getByName( + @GetSetting(true) setting: SettingEntity + ): Promise { + return { data: setting }; + } +} diff --git a/src/common/setting/decorators/setting.admin.decorator.ts b/src/common/setting/decorators/setting.admin.decorator.ts new file mode 100644 index 0000000..e50523d --- /dev/null +++ b/src/common/setting/decorators/setting.admin.decorator.ts @@ -0,0 +1,9 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { SettingNotFoundGuard } from 'src/common/setting/guards/setting.not-found.guard'; +import { SettingPutToRequestGuard } from 'src/common/setting/guards/setting.put-to-request.guard'; + +export function SettingUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestGuard, SettingNotFoundGuard) + ); +} diff --git a/src/common/setting/decorators/setting.decorator.ts b/src/common/setting/decorators/setting.decorator.ts new file mode 100644 index 0000000..a5fa48b --- /dev/null +++ b/src/common/setting/decorators/setting.decorator.ts @@ -0,0 +1,31 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + UseGuards, +} from '@nestjs/common'; +import { SettingNotFoundGuard } from 'src/common/setting/guards/setting.not-found.guard'; +import { + SettingPutToRequestByNameGuard, + SettingPutToRequestGuard, +} from 'src/common/setting/guards/setting.put-to-request.guard'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; + +export const GetSetting = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): SettingDoc => { + const { __setting } = ctx.switchToHttp().getRequest(); + return returnPlain ? __setting.toObject() : __setting; + } +); + +export function SettingGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestGuard, SettingNotFoundGuard) + ); +} + +export function SettingGetByNameGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestByNameGuard, SettingNotFoundGuard) + ); +} diff --git a/src/common/setting/docs/setting.admin.doc.ts b/src/common/setting/docs/setting.admin.doc.ts new file mode 100644 index 0000000..fa2e6d6 --- /dev/null +++ b/src/common/setting/docs/setting.admin.doc.ts @@ -0,0 +1,19 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { SettingDocParamsGet } from 'src/common/setting/constants/setting.doc.constant'; + +export function SettingUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: SettingDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} diff --git a/src/common/setting/docs/setting.doc.ts b/src/common/setting/docs/setting.doc.ts new file mode 100644 index 0000000..5668161 --- /dev/null +++ b/src/common/setting/docs/setting.doc.ts @@ -0,0 +1,49 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { + SettingDocParamsGet, + SettingDocParamsGetByName, +} from 'src/common/setting/constants/setting.doc.constant'; +import { SettingGetSerialization } from 'src/common/setting/serializations/setting.get.serialization'; +import { SettingListSerialization } from 'src/common/setting/serializations/setting.list.serialization'; + +export function SettingListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('setting.list', { + auth: { + jwtAccessToken: false, + }, + response: { + serialization: SettingListSerialization, + }, + }) + ); +} + +export function SettingGetByNameDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.getByName', { + auth: { + jwtAccessToken: false, + }, + request: { + params: SettingDocParamsGetByName, + }, + response: { serialization: SettingGetSerialization }, + }) + ); +} + +export function SettingGetDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.get', { + auth: { + jwtAccessToken: false, + }, + request: { + params: SettingDocParamsGet, + }, + response: { serialization: SettingGetSerialization }, + }) + ); +} diff --git a/src/common/setting/dtos/setting.create.dto.ts b/src/common/setting/dtos/setting.create.dto.ts new file mode 100644 index 0000000..db51bef --- /dev/null +++ b/src/common/setting/dtos/setting.create.dto.ts @@ -0,0 +1,48 @@ +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/common/setting/constants/setting.enum.constant'; + +export class SettingCreateDto { + @IsString() + @IsNotEmpty() + @SafeString() + @Type(() => String) + readonly name: string; + + @IsString() + @IsOptional() + @Type(() => String) + @ApiProperty({ + name: 'description', + examples: ['Maintenance Mode', 'Max Part Number Aws Chunk File'], + description: 'The description about setting', + nullable: true, + }) + readonly description?: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Data type of setting', + example: 'BOOLEAN', + required: true, + enum: ENUM_SETTING_DATA_TYPE, + }) + readonly type: ENUM_SETTING_DATA_TYPE; + + @IsNotEmpty() + @Type(() => String) + @ApiProperty({ + name: 'value', + description: 'The value of setting', + nullable: false, + oneOf: [ + { type: 'string', readOnly: true, examples: ['on', 'off'] }, + { type: 'number', readOnly: true, examples: [100, 200] }, + { type: 'boolean', readOnly: true, examples: [true, false] }, + ], + }) + readonly value: string; +} diff --git a/src/common/setting/dtos/setting.request.dto.ts b/src/common/setting/dtos/setting.request.dto.ts new file mode 100644 index 0000000..affc2b9 --- /dev/null +++ b/src/common/setting/dtos/setting.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class SettingRequestDto { + @ApiProperty({ + name: 'setting', + description: 'setting id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + setting: string; +} diff --git a/src/common/setting/dtos/setting.update-value.dto.ts b/src/common/setting/dtos/setting.update-value.dto.ts new file mode 100644 index 0000000..537f92e --- /dev/null +++ b/src/common/setting/dtos/setting.update-value.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { SettingCreateDto } from './setting.create.dto'; + +export class SettingUpdateValueDto extends OmitType(SettingCreateDto, [ + 'name', +] as const) {} diff --git a/src/common/setting/guards/setting.not-found.guard.ts b/src/common/setting/guards/setting.not-found.guard.ts new file mode 100644 index 0000000..f520db9 --- /dev/null +++ b/src/common/setting/guards/setting.not-found.guard.ts @@ -0,0 +1,24 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; + +@Injectable() +export class SettingNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __setting } = context.switchToHttp().getRequest(); + + if (!__setting) { + throw new NotFoundException({ + statusCode: + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR, + message: 'setting.error.notFound', + }); + } + + return true; + } +} diff --git a/src/common/setting/guards/setting.put-to-request.guard.ts b/src/common/setting/guards/setting.put-to-request.guard.ts new file mode 100644 index 0000000..72e7071 --- /dev/null +++ b/src/common/setting/guards/setting.put-to-request.guard.ts @@ -0,0 +1,39 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@Injectable() +export class SettingPutToRequestGuard implements CanActivate { + constructor(private readonly settingService: SettingService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { setting } = params; + + const check: SettingDoc = await this.settingService.findOneById( + setting + ); + request.__setting = check; + + return true; + } +} + +@Injectable() +export class SettingPutToRequestByNameGuard implements CanActivate { + constructor(private readonly settingService: SettingService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { settingName } = params; + + const check: SettingDoc = await this.settingService.findOneByName( + settingName + ); + request.__setting = check; + + return true; + } +} diff --git a/src/common/setting/interfaces/setting.service.interface.ts b/src/common/setting/interfaces/setting.service.interface.ts new file mode 100644 index 0000000..38da1a9 --- /dev/null +++ b/src/common/setting/interfaces/setting.service.interface.ts @@ -0,0 +1,66 @@ +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { SettingCreateDto } from 'src/common/setting/dtos/setting.create.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; + +export interface ISettingService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + { name, description, type, value }: SettingCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + updateValue( + repository: SettingDoc, + { description, type, value }: SettingUpdateValueDto, + options?: IDatabaseOptions + ): Promise; + + delete(repository: SettingDoc): Promise; + + getValue(setting: SettingDoc): Promise; + + checkValue(value: string, type: ENUM_SETTING_DATA_TYPE): Promise; + + getMaintenance(): Promise; + + getMobileNumberCountryCodeAllowed(): Promise; + + getPasswordAttempt(): Promise; + + getMaxPasswordAttempt(): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts b/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts new file mode 100644 index 0000000..ae5d734 --- /dev/null +++ b/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts @@ -0,0 +1,32 @@ +import { + Injectable, + NestMiddleware, + ServiceUnavailableException, +} from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@Injectable() +export class SettingMaintenanceMiddleware implements NestMiddleware { + constructor(private readonly settingService: SettingService) {} + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const maintenance: boolean = await this.settingService.getMaintenance(); + + if (maintenance) { + throw new ServiceUnavailableException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + message: 'http.serverError.serviceUnavailable', + }); + } + + next(); + } +} diff --git a/src/common/setting/middleware/setting.middleware.module.ts b/src/common/setting/middleware/setting.middleware.module.ts new file mode 100644 index 0000000..fa5a436 --- /dev/null +++ b/src/common/setting/middleware/setting.middleware.module.ts @@ -0,0 +1,50 @@ +import { + Module, + NestModule, + MiddlewareConsumer, + RequestMethod, +} from '@nestjs/common'; +import { SettingMaintenanceMiddleware } from 'src/common/setting/middleware/maintenance/setting.maintenance.middleware'; + +@Module({}) +export class SettingMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply(SettingMaintenanceMiddleware) + .exclude( + { + path: 'api/v:version*/user/login', + method: RequestMethod.POST, + }, + { + path: 'api/user/login', + method: RequestMethod.POST, + }, + { + path: 'api/v:version*/user/refresh', + method: RequestMethod.POST, + }, + { + path: 'api/user/refresh', + method: RequestMethod.POST, + }, + { + path: 'api/v:version*/admin/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/admin/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/v:version*/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/setting/(.*)', + method: RequestMethod.ALL, + } + ) + .forRoutes('*'); + } +} diff --git a/src/common/setting/repository/entities/setting.entity.ts b/src/common/setting/repository/entities/setting.entity.ts new file mode 100644 index 0000000..97c603e --- /dev/null +++ b/src/common/setting/repository/entities/setting.entity.ts @@ -0,0 +1,43 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { Document } from 'mongoose'; + +export const SettingDatabaseName = 'settings'; + +@DatabaseEntity({ collection: SettingDatabaseName }) +export class SettingEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + trim: true, + type: String, + }) + name: string; + + @Prop({ + required: false, + type: String, + }) + description?: string; + + @Prop({ + required: false, + type: String, + enum: ENUM_SETTING_DATA_TYPE, + }) + type: ENUM_SETTING_DATA_TYPE; + + @Prop({ + required: true, + trim: true, + type: String, + }) + value: string; +} + +export const SettingSchema = SchemaFactory.createForClass(SettingEntity); + +export type SettingDoc = SettingEntity & Document; diff --git a/src/common/setting/repository/repositories/setting.repository.ts b/src/common/setting/repository/repositories/setting.repository.ts new file mode 100644 index 0000000..fc8d9a8 --- /dev/null +++ b/src/common/setting/repository/repositories/setting.repository.ts @@ -0,0 +1,21 @@ +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 { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; + +@Injectable() +export class SettingRepository extends DatabaseMongoUUIDRepositoryAbstract< + SettingEntity, + SettingDoc +> { + constructor( + @DatabaseModel(SettingEntity.name) + private readonly settingModel: Model + ) { + super(settingModel); + } +} diff --git a/src/common/setting/repository/setting.repository.module.ts b/src/common/setting/repository/setting.repository.module.ts new file mode 100644 index 0000000..15f9db0 --- /dev/null +++ b/src/common/setting/repository/setting.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + SettingEntity, + SettingSchema, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingRepository } from 'src/common/setting/repository/repositories/setting.repository'; + +@Module({ + providers: [SettingRepository], + exports: [SettingRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: SettingEntity.name, + schema: SettingSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class SettingRepositoryModule {} diff --git a/src/common/setting/serializations/setting.get.serialization.ts b/src/common/setting/serializations/setting.get.serialization.ts new file mode 100644 index 0000000..cf15f5d --- /dev/null +++ b/src/common/setting/serializations/setting.get.serialization.ts @@ -0,0 +1,74 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Transform } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; + +export class SettingGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Name of setting', + example: 'MaintenanceOn', + required: true, + }) + readonly name: string; + + @ApiProperty({ + description: 'Description of setting', + example: 'Maintenance Mode', + required: false, + }) + readonly description?: string; + + @ApiProperty({ + description: 'Data type of setting', + example: 'BOOLEAN', + required: true, + enum: ENUM_SETTING_DATA_TYPE, + }) + readonly type: ENUM_SETTING_DATA_TYPE; + + @ApiProperty({ + description: 'Value of string, can be type string/boolean/number', + oneOf: [ + { type: 'string', readOnly: true, examples: ['on', 'off'] }, + { type: 'number', readOnly: true, examples: [100, 200] }, + { type: 'boolean', readOnly: true, examples: [true, false] }, + ], + required: true, + }) + @Transform(({ value, obj }) => { + const regex = /^-?\d+$/; + const checkNum = regex.test(value); + + if ( + obj.type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (value === 'true' || value === 'false') + ) { + return value === 'true' ? true : false; + } else if (obj.type === ENUM_SETTING_DATA_TYPE.NUMBER && checkNum) { + return Number(value); + } else if (obj.type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) { + return value.split(','); + } + + return value; + }) + readonly value: string | number | boolean; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: false, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/common/setting/serializations/setting.list.serialization.ts b/src/common/setting/serializations/setting.list.serialization.ts new file mode 100644 index 0000000..8ff828b --- /dev/null +++ b/src/common/setting/serializations/setting.list.serialization.ts @@ -0,0 +1,3 @@ +import { SettingGetSerialization } from './setting.get.serialization'; + +export class SettingListSerialization extends SettingGetSerialization {} diff --git a/src/common/setting/services/setting.service.ts b/src/common/setting/services/setting.service.ts new file mode 100644 index 0000000..873dc83 --- /dev/null +++ b/src/common/setting/services/setting.service.ts @@ -0,0 +1,170 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { SettingCreateDto } from 'src/common/setting/dtos/setting.create.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { ISettingService } from 'src/common/setting/interfaces/setting.service.interface'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingRepository } from 'src/common/setting/repository/repositories/setting.repository'; + +@Injectable() +export class SettingService implements ISettingService { + private readonly mobileNumberCountryCodeAllowed: string[]; + private readonly passwordAttempt: boolean; + private readonly maxPasswordAttempt: number; + + constructor( + private readonly settingRepository: SettingRepository, + private readonly configService: ConfigService, + private readonly helperNumberService: HelperNumberService + ) { + this.mobileNumberCountryCodeAllowed = this.configService.get( + 'user.mobileNumberCountryCodeAllowed' + ); + this.passwordAttempt = this.configService.get( + 'auth.password.attempt' + ); + this.maxPasswordAttempt = this.configService.get( + 'auth.password.maxAttempt' + ); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.settingRepository.findAll(find, options); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.settingRepository.findOneById(_id, options); + } + + async findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.settingRepository.findOne( + { name }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.settingRepository.getTotal(find, options); + } + + async create( + { name, description, type, value }: SettingCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: SettingEntity = new SettingEntity(); + create.name = name; + create.description = description ?? undefined; + create.value = value; + create.type = type; + + return this.settingRepository.create(create, options); + } + + async updateValue( + repository: SettingDoc, + { description, type, value }: SettingUpdateValueDto + ): Promise { + repository.description = description; + repository.type = type; + repository.value = value; + + return this.settingRepository.save(repository); + } + + async delete(repository: SettingDoc): Promise { + return this.settingRepository.softDelete(repository); + } + + async getValue(setting: SettingDoc): Promise { + if ( + setting.type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (setting.value === 'true' || setting.value === 'false') + ) { + return (setting.value === 'true') as any; + } else if ( + setting.type === ENUM_SETTING_DATA_TYPE.NUMBER && + this.helperNumberService.check(setting.value) + ) { + return this.helperNumberService.create(setting.value) as any; + } else if (setting.type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) { + return setting.value.split(',') as any; + } + + return setting.value as any; + } + + async checkValue( + value: string, + type: ENUM_SETTING_DATA_TYPE + ): Promise { + if ( + type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (value === 'true' || value === 'false') + ) { + return true; + } else if ( + type === ENUM_SETTING_DATA_TYPE.NUMBER && + this.helperNumberService.check(value) + ) { + return true; + } else if ( + (type === ENUM_SETTING_DATA_TYPE.STRING || + type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) && + typeof value === 'string' + ) { + return true; + } + + return false; + } + + async getMaintenance(): Promise { + const setting: SettingDoc = + await this.settingRepository.findOne({ + name: 'maintenance', + }); + + return this.getValue(setting); + } + + async getMobileNumberCountryCodeAllowed(): Promise { + return this.mobileNumberCountryCodeAllowed; + } + + async getPasswordAttempt(): Promise { + return this.passwordAttempt; + } + + async getMaxPasswordAttempt(): Promise { + return this.maxPasswordAttempt; + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.settingRepository.deleteMany(find, options); + } +} diff --git a/src/common/setting/setting.module.ts b/src/common/setting/setting.module.ts new file mode 100644 index 0000000..55c2f0f --- /dev/null +++ b/src/common/setting/setting.module.ts @@ -0,0 +1,13 @@ +import { Global, Module } from '@nestjs/common'; +import { SettingMiddlewareModule } from 'src/common/setting/middleware/setting.middleware.module'; +import { SettingRepositoryModule } from 'src/common/setting/repository/setting.repository.module'; +import { SettingService } from './services/setting.service'; + +@Global() +@Module({ + imports: [SettingRepositoryModule, SettingMiddlewareModule], + exports: [SettingService], + providers: [SettingService], + controllers: [], +}) +export class SettingModule {} diff --git a/src/configs/app.config.ts b/src/configs/app.config.ts new file mode 100644 index 0000000..8289019 --- /dev/null +++ b/src/configs/app.config.ts @@ -0,0 +1,31 @@ +import { registerAs } from '@nestjs/config'; +import { version } from 'package.json'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; + +export default registerAs( + 'app', + (): Record => ({ + name: process.env.APP_NAME ?? 'nest', + env: process.env.APP_ENV ?? ENUM_APP_ENVIRONMENT.DEVELOPMENT, + language: process.env.APP_LANGUAGE?.split(',') ?? [APP_LANGUAGE], + + repoVersion: version, + versioning: { + enable: process.env.HTTP_VERSIONING_ENABLE === 'true' ?? false, + prefix: 'v', + version: process.env.HTTP_VERSION ?? '1', + }, + + globalPrefix: '/api', + 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, + }, + + jobEnable: process.env.JOB_ENABLE === 'true' ?? false, + }) +); diff --git a/src/configs/auth.config.ts b/src/configs/auth.config.ts new file mode 100644 index 0000000..39c2665 --- /dev/null +++ b/src/configs/auth.config.ts @@ -0,0 +1,62 @@ +import { registerAs } from '@nestjs/config'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; + +export default registerAs( + 'auth', + (): Record => ({ + accessToken: { + secretKey: process.env.AUTH_JWT_ACCESS_TOKEN_SECRET_KEY ?? '123456', + expirationTime: seconds( + process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED ?? '15m' + ), // recommendation for production is 15m + notBeforeExpirationTime: seconds('0'), // keep it in zero value + + encryptKey: process.env.AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV, + }, + + refreshToken: { + secretKey: + process.env.AUTH_JWT_REFRESH_TOKEN_SECRET_KEY ?? '123456000', + expirationTime: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRED ?? '7d' + ), // recommendation for production is 7d + expirationTimeRememberMe: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED ?? '30d' + ), // recommendation for production is 30d + notBeforeExpirationTime: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION ?? + '15m' + ), // recommendation for production is 15m + + encryptKey: process.env.AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV, + }, + + subject: process.env.AUTH_JWT_SUBJECT ?? 'nestDevelopment', + audience: process.env.AUTH_JWT_AUDIENCE ?? 'https://example.com', + issuer: process.env.AUTH_JWT_ISSUER ?? 'nest', + prefixAuthorization: 'Bearer', + payloadEncryption: + process.env.AUTH_JWT_PAYLOAD_ENCRYPT === 'true' ? true : false, + + permissionToken: { + headerName: 'x-permission-token', + secretKey: process.env.AUTH_PERMISSION_TOKEN_SECRET_KEY ?? '123456', + expirationTime: seconds( + process.env.AUTH_PERMISSION_TOKEN_EXPIRED ?? '5m' + ), // recommendation for production is 5m + notBeforeExpirationTime: seconds('0'), // keep it in zero value + + encryptKey: process.env.AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV, + }, + + password: { + attempt: true, + maxAttempt: 3, + saltLength: 8, + expiredIn: seconds('182d'), // recommendation for production is 182 days + }, + }) +); diff --git a/src/configs/aws.config.ts b/src/configs/aws.config.ts new file mode 100644 index 0000000..6c52af2 --- /dev/null +++ b/src/configs/aws.config.ts @@ -0,0 +1,16 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'aws', + (): Record => ({ + credential: { + key: process.env.AWS_CREDENTIAL_KEY, + secret: process.env.AWS_CREDENTIAL_SECRET, + }, + s3: { + bucket: process.env.AWS_S3_BUCKET ?? 'nests3', + region: process.env.AWS_S3_REGION, + baseUrl: `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com`, + }, + }) +); diff --git a/src/configs/database.config.ts b/src/configs/database.config.ts new file mode 100644 index 0000000..ee0fa8e --- /dev/null +++ b/src/configs/database.config.ts @@ -0,0 +1,13 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'database', + (): Record => ({ + host: process.env?.DATABASE_HOST ?? 'mongodb://localhost:27017', + name: process.env?.DATABASE_NAME ?? 'nest', + user: process.env?.DATABASE_USER, + password: process?.env.DATABASE_PASSWORD, + debug: process.env.DATABASE_DEBUG === 'true', + options: process.env?.DATABASE_OPTIONS, + }) +); diff --git a/src/configs/debugger.config.ts b/src/configs/debugger.config.ts new file mode 100644 index 0000000..31cfa47 --- /dev/null +++ b/src/configs/debugger.config.ts @@ -0,0 +1,22 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'debugger', + (): Record => ({ + http: { + writeIntoFile: process.env.DEBUGGER_HTTP_WRITE_INTO_FILE === 'true', + writeIntoConsole: + process.env.DEBUGGER_HTTP_WRITE_INTO_CONSOLE === 'true', + maxFiles: 5, + maxSize: '2M', + }, + system: { + writeIntoFile: + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE === 'true', + writeIntoConsole: + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE === 'true', + maxFiles: '7d', + maxSize: '2m', + }, + }) +); diff --git a/src/configs/doc.config.ts b/src/configs/doc.config.ts new file mode 100644 index 0000000..678822b --- /dev/null +++ b/src/configs/doc.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'doc', + (): Record => ({ + name: `${process.env.APP_NAME} APIs Specification`, + description: 'Section for describe whole APIs', + version: '1.0', + prefix: '/docs', + }) +); diff --git a/src/configs/file.config.ts b/src/configs/file.config.ts new file mode 100644 index 0000000..28df479 --- /dev/null +++ b/src/configs/file.config.ts @@ -0,0 +1,26 @@ +import { registerAs } from '@nestjs/config'; +import bytes from 'bytes'; + +// if we use was api gateway, there has limitation of the payload size +// the payload size 10mb +export default registerAs( + 'file', + (): Record => ({ + image: { + maxFileSize: bytes('1mb'), // 1mb + maxFiles: 3, // 3 files + }, + excel: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + audio: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + video: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + }) +); diff --git a/src/configs/helper.config.ts b/src/configs/helper.config.ts new file mode 100644 index 0000000..d9c6192 --- /dev/null +++ b/src/configs/helper.config.ts @@ -0,0 +1,16 @@ +import { registerAs } from '@nestjs/config'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; + +export default registerAs( + 'helper', + (): Record => ({ + salt: { + length: 8, + }, + jwt: { + secretKey: '123456', + expirationTime: seconds('1h'), + notBeforeExpirationTime: seconds('0'), + }, + }) +); diff --git a/src/configs/index.ts b/src/configs/index.ts new file mode 100644 index 0000000..0bd099d --- /dev/null +++ b/src/configs/index.ts @@ -0,0 +1,23 @@ +import AppConfig from './app.config'; +import AuthConfig from './auth.config'; +import DatabaseConfig from './database.config'; +import HelperConfig from './helper.config'; +import AwsConfig from './aws.config'; +import UserConfig from './user.config'; +import FileConfig from './file.config'; +import RequestConfig from './request.config'; +import DocConfig from './doc.config'; +import DebuggerConfig from './debugger.config'; + +export default [ + AppConfig, + AuthConfig, + DatabaseConfig, + HelperConfig, + AwsConfig, + UserConfig, + RequestConfig, + FileConfig, + DocConfig, + DebuggerConfig, +]; diff --git a/src/configs/request.config.ts b/src/configs/request.config.ts new file mode 100644 index 0000000..2a62670 --- /dev/null +++ b/src/configs/request.config.ts @@ -0,0 +1,95 @@ +import { registerAs } from '@nestjs/config'; +import bytes from 'bytes'; +import ms from 'ms'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; + +export default registerAs( + 'request', + (): Record => ({ + body: { + json: { + maxFileSize: bytes('100kb'), // 100kb + }, + raw: { + maxFileSize: bytes('5.5mb'), // 5.5mb + }, + text: { + maxFileSize: bytes('100kb'), // 100kb + }, + urlencoded: { + maxFileSize: bytes('100kb'), // 100kb + }, + }, + timestamp: { + toleranceTimeInMs: ms('5m'), // 5 mins + }, + timeout: ms('30s'), // 30s based on ms module + userAgent: { + os: [ + 'Mobile', + 'Mac OS', + 'Windows', + 'UNIX', + 'Linux', + 'iOS', + 'Android', + ], + browser: [ + 'IE', + 'Safari', + 'Edge', + 'Opera', + 'Chrome', + 'Firefox', + 'Samsung Browser', + 'UCBrowser', + ], + }, + cors: { + allowMethod: [ + ENUM_REQUEST_METHOD.GET, + ENUM_REQUEST_METHOD.DELETE, + ENUM_REQUEST_METHOD.PUT, + ENUM_REQUEST_METHOD.PATCH, + ENUM_REQUEST_METHOD.POST, + ], + allowOrigin: '*', // allow all origin + // allowOrigin: [/example\.com(\:\d{1,4})?$/], // allow all subdomain, and all port + // allowOrigin: [/example\.com$/], // allow all subdomain without port + allowHeader: [ + 'Accept', + 'Accept-Language', + 'Content-Language', + 'Content-Type', + 'Origin', + 'Authorization', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Origin', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Credentials', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Referer', + 'Host', + 'X-Requested-With', + 'x-custom-lang', + 'x-timestamp', + 'x-api-key', + 'x-timezone', + 'x-request-id', + 'x-version', + 'x-repo-version', + 'x-permission-token', + 'X-Response-Time', + 'user-agent', + ], + }, + throttle: { + ttl: seconds('500'), // 0.5 secs + limit: 10, // max request per reset time + }, + }) +); diff --git a/src/configs/user.config.ts b/src/configs/user.config.ts new file mode 100644 index 0000000..e10fe06 --- /dev/null +++ b/src/configs/user.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'user', + (): Record => ({ + uploadPath: '/user', + mobileNumberCountryCodeAllowed: ['628', '658'], + }) +); diff --git a/src/health/controllers/health.controller.ts b/src/health/controllers/health.controller.ts new file mode 100644 index 0000000..c017096 --- /dev/null +++ b/src/health/controllers/health.controller.ts @@ -0,0 +1,117 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + DiskHealthIndicator, + HealthCheck, + HealthCheckService, + MemoryHealthIndicator, + MongooseHealthIndicator, +} from '@nestjs/terminus'; +import { Connection } from 'mongoose'; +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/health/docs/health.doc'; +import { HealthAwsS3Indicator } from 'src/health/indicators/health.aws-s3.indicator'; +import { HealthSerialization } from 'src/health/serializations/health.serialization'; + +@ApiTags('health') +@Controller({ + version: VERSION_NEUTRAL, + path: '/health', +}) +export class HealthController { + constructor( + @DatabaseConnection() private readonly databaseConnection: Connection, + private readonly health: HealthCheckService, + private readonly memoryHealthIndicator: MemoryHealthIndicator, + private readonly diskHealthIndicator: DiskHealthIndicator, + private readonly mongooseIndicator: MongooseHealthIndicator, + private readonly awsS3Indicator: HealthAwsS3Indicator + ) {} + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/aws') + async checkAws(): Promise { + const data = await this.health.check([ + () => this.awsS3Indicator.isHealthy('awsS3Bucket'), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/database') + async checkDatabase(): Promise { + const data = await this.health.check([ + () => + this.mongooseIndicator.pingCheck('database', { + connection: this.databaseConnection, + }), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/memory-heap') + async checkMemoryHeap(): Promise { + const data = await this.health.check([ + () => + this.memoryHealthIndicator.checkHeap( + 'memoryHeap', + 300 * 1024 * 1024 + ), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/memory-rss') + async checkMemoryRss(): Promise { + const data = await this.health.check([ + () => + this.memoryHealthIndicator.checkRSS( + 'memoryRss', + 300 * 1024 * 1024 + ), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/storage') + async checkStorage(): Promise { + const data = await this.health.check([ + () => + this.diskHealthIndicator.checkStorage('diskHealth', { + thresholdPercent: 0.75, + path: '/', + }), + ]); + + return { + data, + }; + } +} diff --git a/src/health/docs/health.doc.ts b/src/health/docs/health.doc.ts new file mode 100644 index 0000000..04b3373 --- /dev/null +++ b/src/health/docs/health.doc.ts @@ -0,0 +1,14 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { HealthSerialization } from 'src/health/serializations/health.serialization'; + +export function HealthCheckDoc(): MethodDecorator { + return applyDecorators( + Doc('health.check', { + auth: { + jwtAccessToken: false, + }, + response: { serialization: HealthSerialization }, + }) + ); +} diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 0000000..770e9f8 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AwsModule } from 'src/common/aws/aws.module'; +import { HealthAwsS3Indicator } from 'src/health/indicators/health.aws-s3.indicator'; + +@Module({ + providers: [HealthAwsS3Indicator], + exports: [HealthAwsS3Indicator], + imports: [AwsModule], +}) +export class HealthModule {} diff --git a/src/health/indicators/health.aws-s3.indicator.ts b/src/health/indicators/health.aws-s3.indicator.ts new file mode 100644 index 0000000..62967ab --- /dev/null +++ b/src/health/indicators/health.aws-s3.indicator.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; + +@Injectable() +export class HealthAwsS3Indicator extends HealthIndicator { + constructor(private readonly awsS3Service: AwsS3Service) { + super(); + } + + async isHealthy(key: string): Promise { + try { + await this.awsS3Service.checkConnection(); + return this.getStatus(key, true); + } catch (err: unknown) { + throw new HealthCheckError( + 'HealthAwsS3Indicator failed', + this.getStatus(key, false) + ); + } + } +} diff --git a/src/health/serializations/health.serialization.ts b/src/health/serializations/health.serialization.ts new file mode 100644 index 0000000..97d73fd --- /dev/null +++ b/src/health/serializations/health.serialization.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class HealthSerialization { + @ApiProperty({ + example: 'ok', + }) + status: string; + + @ApiProperty({ + example: { + awsBucket: { + status: 'up', + }, + }, + }) + info: Record; + + @ApiProperty({ + example: {}, + }) + error: Record; + + @ApiProperty({ + example: { + awsBucket: { + status: 'up', + }, + }, + }) + details: Record; +} diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts new file mode 100644 index 0000000..0e6d751 --- /dev/null +++ b/src/jobs/jobs.module.ts @@ -0,0 +1,27 @@ +import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { JobsRouterModule } from './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 new file mode 100644 index 0000000..f745d22 --- /dev/null +++ b/src/jobs/router/jobs.router.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { ApiKeyInactiveTask } from 'src/common/api-key/tasks/api-key.inactive.task'; + +@Module({ + providers: [ApiKeyInactiveTask], + exports: [], + imports: [ApiKeyModule], + controllers: [], +}) +export class JobsRouterModule {} diff --git a/src/languages/en/apiKey.json b/src/languages/en/apiKey.json new file mode 100644 index 0000000..3307592 --- /dev/null +++ b/src/languages/en/apiKey.json @@ -0,0 +1,13 @@ +{ + "error": { + "timestampInvalid": "Request timestamp not acceptable", + "keyNeeded": "Need API Key", + "prefixInvalid": "Prefix API Key invalid", + "schemaInvalid": "API Key Schema invalid", + "timestampNotMatchWithRequest": "Timestamp not match with request", + "notFound": "Auth API not found", + "inactive": "Auth API Inactive", + "invalid": "Invalid API Key", + "exist": "API Key Exist" + } +} \ No newline at end of file diff --git a/src/languages/en/app.json b/src/languages/en/app.json new file mode 100644 index 0000000..06fe878 --- /dev/null +++ b/src/languages/en/app.json @@ -0,0 +1,4 @@ +{ + "hello": "This is test endpoint service {serviceName}.", + "helloTimeout": "This is test endpoint service {serviceName} timeout." +} diff --git a/src/languages/en/auth.json b/src/languages/en/auth.json new file mode 100644 index 0000000..7761528 --- /dev/null +++ b/src/languages/en/auth.json @@ -0,0 +1,15 @@ +{ + "enum": { + "accessFor": "Enum access for succeed" + }, + "error": { + "passwordExpired": + "Password expired, go reset password", + "permissionForbidden": + "Permission not allowed", + "accessForForbidden": "Access for not allowed", + "accessTokenUnauthorized": "Access Token UnAuthorized", + "refreshTokenUnauthorized": "Refresh Token UnAuthorized" + } +} + diff --git a/src/languages/en/file.json b/src/languages/en/file.json new file mode 100644 index 0000000..ee13f47 --- /dev/null +++ b/src/languages/en/file.json @@ -0,0 +1,10 @@ +{ + "error": { + "notFound": "File not found", + "maxSize": "File size too big", + "maxFiles": "Files are to many", + "mimeInvalid": "File extension not valid", + "needExtractFirst": "Extract data needed", + "validationDto": "Import Data invalid" + } +} diff --git a/src/languages/en/health.json b/src/languages/en/health.json new file mode 100644 index 0000000..bbf65aa --- /dev/null +++ b/src/languages/en/health.json @@ -0,0 +1,3 @@ +{ + "check": "Healthy succeed" +} \ No newline at end of file diff --git a/src/languages/en/http.json b/src/languages/en/http.json new file mode 100644 index 0000000..b25f068 --- /dev/null +++ b/src/languages/en/http.json @@ -0,0 +1,67 @@ +{ + "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" + }, + "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/en/message.json b/src/languages/en/message.json new file mode 100644 index 0000000..8083371 --- /dev/null +++ b/src/languages/en/message.json @@ -0,0 +1,5 @@ +{ + "enum": { + "languages": "message enum languages" + } +} \ No newline at end of file diff --git a/src/languages/en/middleware.json b/src/languages/en/middleware.json new file mode 100644 index 0000000..30f52ea --- /dev/null +++ b/src/languages/en/middleware.json @@ -0,0 +1,7 @@ +{ + "error": { + "userAgentInvalid": "Request user agent not acceptable", + "userAgentOsInvalid": "Request user agent OS not acceptable", + "userAgentBrowserInvalid": "Request user agent Browser not acceptable" + } +} \ No newline at end of file diff --git a/src/languages/en/permission.json b/src/languages/en/permission.json new file mode 100644 index 0000000..fd9f191 --- /dev/null +++ b/src/languages/en/permission.json @@ -0,0 +1,11 @@ +{ + "list": "Permission list succeed", + "get": "Permission get succeed", + "update": "Permission update succeed", + "active": "Permission active succeed", + "inactive": "Permission inactive succeed", + "error": { + "notFound": "Permission not found", + "active": "Permission active status invalid" + } +} diff --git a/src/languages/en/request.json b/src/languages/en/request.json new file mode 100644 index 0000000..89602ec --- /dev/null +++ b/src/languages/en/request.json @@ -0,0 +1,30 @@ +{ + "validation": "Validation errors", + "min": "{property} has less elements than the minimum allowed.", + "max": "{property} has more elements than the maximum allowed.", + "maxLength": "{property} has more elements than the maximum allowed.", + "minLength": "{property} has less elements than the minimum allowed.", + "isString": "{property} should be a type of string.", + "isNotEmpty": "{property} cannot be empty.", + "isLowercase": "{property} should be lowercase.", + "isOptional": "{property} is optional.", + "isPositive": "{property} should be a positive number.", + "isEmail": "{property} should be a type of email.", + "isInt": "{property} should be a number.", + "isNumberString": "{property} should be a number.", + "isNumber": "{property} should be a number {value}.", + "isMongoId": "{property} should reference with mongo object id.", + "isBoolean": "{property} should be a boolean", + "IsStartWith": "{property} should start with {value}", + "isEnum": "{property} don't match with enum", + "isObject": "{property} should be a object", + "isArray": "{property} should be a array", + "arrayNotEmpty": "{property} array is not empty", + "minDate": "{property} has less date than the minimum allowed.", + "maxDate": "{property} has more elements than the maximum allowed.", + "isDate": "{property} should be a date", + "minDateGreaterThan": "{property} has less date than the {value}", + "isPasswordStrong": "{property} must have strong pattern", + "isPasswordMedium": "{property} must have medium pattern", + "isPasswordWeak": "{property} must have weak pattern" +} diff --git a/src/languages/en/role.json b/src/languages/en/role.json new file mode 100644 index 0000000..bc83383 --- /dev/null +++ b/src/languages/en/role.json @@ -0,0 +1,16 @@ +{ + "list": "List Role Success.", + "get": "Get Role Success.", + "create": "Create Succeed", + "delete": "Delete Succeed", + "update": "Update Succeed", + "inactive": "Inactive Succeed", + "active": "Active Succeed", + "error": { + "notFound": "Role not found", + "exist": "Role exist", + "active": "Role active invalid", + "used": "Role in used", + "inactive": "Role is inactive" + } +} diff --git a/src/languages/en/setting.json b/src/languages/en/setting.json new file mode 100644 index 0000000..59b2dc9 --- /dev/null +++ b/src/languages/en/setting.json @@ -0,0 +1,9 @@ +{ + "list": "List Setting Success.", + "get": "Get Setting Success.", + "getByName": "Get Setting by name Success.", + "update": "Update Succeed", + "error": { + "notFound": "Setting not found" + } +} diff --git a/src/languages/en/user.json b/src/languages/en/user.json new file mode 100644 index 0000000..24a856e --- /dev/null +++ b/src/languages/en/user.json @@ -0,0 +1,25 @@ +{ + "list": "List User Success.", + "get": "Get User Success.", + "create": "Create User Success.", + "delete": "Delete User Success.", + "update": "Update User Success.", + "profile": "Profile Success", + "upload": "Upload Success", + "inactive": "Inactive Succeed", + "active": "Active Succeed", + "login": "Login success.", + "refresh": "Refresh token success", + "signUp": "Sign up Success", + "error": { + "notFound": "User not found.", + "emailExist": "Email user used", + "mobileNumberExist": "Mobile Number user used", + "active": "User status active invalid", + "exist": "User exist", + "passwordNotMatch": "Password not match", + "newPasswordMustDifference": "Old password must difference", + "inactive": "User is inactive", + "passwordExpired": "User password expired" + } +} diff --git a/src/languages/id/app.json b/src/languages/id/app.json new file mode 100644 index 0000000..06fe878 --- /dev/null +++ b/src/languages/id/app.json @@ -0,0 +1,4 @@ +{ + "hello": "This is test endpoint service {serviceName}.", + "helloTimeout": "This is test endpoint service {serviceName} timeout." +} diff --git a/src/languages/id/request.json b/src/languages/id/request.json new file mode 100644 index 0000000..8f3bfd5 --- /dev/null +++ b/src/languages/id/request.json @@ -0,0 +1,30 @@ +{ + "default": "Validation errors", + "min": "{property} has less elements than the minimum allowed.", + "max": "{property} has more elements than the maximum allowed.", + "maxLength": "{property} has more elements than the maximum allowed.", + "minLength": "{property} has less elements than the minimum allowed.", + "isString": "{property} should be a type of string.", + "isNotEmpty": "{property} cannot be empty.", + "isLowercase": "{property} should be lowercase.", + "isOptional": "{property} is optional.", + "isPositive": "{property} should be a positive number.", + "isEmail": "{property} should be a type of email.", + "isInt": "{property} should be a number.", + "isNumberString": "{property} should be a number.", + "isNumber": "{property} should be a number {value}.", + "isMongoId": "{property} should reference with mongo object id.", + "isBoolean": "{property} should be a boolean", + "IsStartWith": "{property} should start with {value}", + "isEnum": "{property} don't match with enum", + "isObject": "{property} should be a object", + "isArray": "{property} should be a array", + "arrayNotEmpty": "{property} array is not empty", + "minDate": "{property} has less date than the minimum allowed.", + "maxDate": "{property} has more elements than the maximum allowed.", + "isDate": "{property} should be a date", + "minDateGreaterThan": "{property} has less date than the {value}", + "isPasswordStrong": "{property} must have strong pattern", + "isPasswordMedium": "{property} must have medium pattern", + "isPasswordWeak": "{property} must have weak pattern" +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..69048c2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,74 @@ +import { NestApplication, NestFactory } from '@nestjs/core'; +import { Logger, VersioningType } from '@nestjs/common'; +import { AppModule } from 'src/app/app.module'; +import { ConfigService } from '@nestjs/config'; +import { useContainer } from 'class-validator'; +import swaggerInit from './swagger'; + +async function bootstrap() { + const app: NestApplication = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + const databaseUri: string = configService.get('database.host'); + const env: string = configService.get('app.env'); + const host: string = configService.get('app.http.host'); + const port: number = configService.get('app.http.port'); + const globalPrefix: string = configService.get('app.globalPrefix'); + const versioningPrefix: string = configService.get( + 'app.versioning.prefix' + ); + const version: string = configService.get('app.versioning.version'); + + // enable + const httpEnable: boolean = configService.get('app.http.enable'); + const versionEnable: string = configService.get( + 'app.versioning.enable' + ); + const jobEnable: boolean = configService.get('app.jobEnable'); + + const logger = new Logger(); + process.env.NODE_ENV = env; + + // Global + app.setGlobalPrefix(globalPrefix); + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + + // Versioning + if (versionEnable) { + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: version, + prefix: versioningPrefix, + }); + } + + // Swagger + await swaggerInit(app); + + // Listen + await app.listen(port, host); + + logger.log(`==========================================================`); + + logger.log(`Environment Variable`, 'NestApplication'); + logger.log(JSON.parse(JSON.stringify(process.env)), 'NestApplication'); + + logger.log(`==========================================================`); + + logger.log(`Job is ${jobEnable}`, 'NestApplication'); + logger.log( + `Http is ${httpEnable}, ${ + httpEnable ? 'routes registered' : 'no routes registered' + }`, + 'NestApplication' + ); + logger.log(`Http versioning is ${versionEnable}`, 'NestApplication'); + + logger.log( + `Http Server running on ${await app.getUrl()}`, + 'NestApplication' + ); + logger.log(`Database uri ${databaseUri}`, 'NestApplication'); + + logger.log(`==========================================================`); +} +bootstrap(); diff --git a/src/migration/migration.module.ts b/src/migration/migration.module.ts new file mode 100644 index 0000000..0c4fe72 --- /dev/null +++ b/src/migration/migration.module.ts @@ -0,0 +1,34 @@ +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 { MigrationPermissionSeed } from 'src/migration/seeds/migration.permission.seed'; +import { MigrationRoleSeed } from 'src/migration/seeds/migration.role.seed'; +import { MigrationSettingSeed } from 'src/migration/seeds/migration.setting.seed'; +import { MigrationUserSeed } from 'src/migration/seeds/migration.user.seed'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserModule } from 'src/modules/user/user.module'; + +@Module({ + imports: [ + CommonModule, + CommandModule, + ApiKeyModule, + AuthModule, + PermissionModule, + UserModule, + RoleModule, + ], + providers: [ + MigrationApiKeySeed, + MigrationSettingSeed, + MigrationPermissionSeed, + MigrationRoleSeed, + MigrationUserSeed, + ], + exports: [], +}) +export class MigrationModule {} diff --git a/src/migration/seeds/migration.api-key.seed.ts b/src/migration/seeds/migration.api-key.seed.ts new file mode 100644 index 0000000..1a32231 --- /dev/null +++ b/src/migration/seeds/migration.api-key.seed.ts @@ -0,0 +1,49 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { UserService } from 'src/modules/user/services/user.service'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class MigrationApiKeySeed { + constructor( + private readonly userService: UserService, + private readonly apiKeyService: ApiKeyService + ) {} + + @Command({ + command: 'seed:apikey', + describe: 'seeds apikeys', + }) + async seeds(): Promise { + try { + const user: UserDoc = await this.userService.findOneByUsername( + 'superadmin' + ); + await this.apiKeyService.createRaw(user._id, { + name: 'Api Key Migration', + description: 'From migration', + key: 'qwertyuiop12345zxcvbnmkjh', + secret: '5124512412412asdasdasdasdasdASDASDASD', + }); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:apikey', + describe: 'remove apikeys', + }) + async remove(): Promise { + try { + await this.apiKeyService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.permission.seed.ts b/src/migration/seeds/migration.permission.seed.ts new file mode 100644 index 0000000..47cae96 --- /dev/null +++ b/src/migration/seeds/migration.permission.seed.ts @@ -0,0 +1,55 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@Injectable() +export class MigrationPermissionSeed { + constructor(private readonly permissionService: PermissionService) {} + + @Command({ + command: 'seed:permission', + describe: 'seed permissions', + }) + async seeds(): Promise { + try { + const permissions: string[] = Object.values(ENUM_AUTH_PERMISSIONS); + const group: string[] = Object.values(ENUM_PERMISSION_GROUP); + + const data: PermissionCreateDto[] = permissions.map((val) => { + const dto: PermissionCreateDto = new PermissionCreateDto(); + + dto.code = val; + dto.description = `${val.replace('_', ' ')} description`; + dto.group = group.find((l: string) => + val.startsWith(l) + ) as ENUM_PERMISSION_GROUP; + + return dto; + }) as PermissionEntity[]; + + await this.permissionService.createMany(data); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:permission', + describe: 'remove permissions', + }) + async remove(): Promise { + try { + await this.permissionService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.role.seed.ts b/src/migration/seeds/migration.role.seed.ts new file mode 100644 index 0000000..1514ed7 --- /dev/null +++ b/src/migration/seeds/migration.role.seed.ts @@ -0,0 +1,61 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; + +@Injectable() +export class MigrationRoleSeed { + constructor( + private readonly permissionService: PermissionService, + private readonly roleService: RoleService + ) {} + + @Command({ + command: 'seed:role', + describe: 'seed roles', + }) + async seeds(): Promise { + const permissions: PermissionEntity[] = + await this.permissionService.findAll(); + const permissionsMap = permissions.map((val) => val._id); + + const dataAdmin: RoleCreateDto[] = [ + { + name: 'admin', + permissions: permissionsMap, + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }, + { + name: 'user', + permissions: [], + accessFor: ENUM_AUTH_ACCESS_FOR.USER, + }, + ]; + + try { + await this.roleService.createMany(dataAdmin); + await this.roleService.createSuperAdmin(); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:role', + describe: 'remove roles', + }) + async remove(): Promise { + try { + await this.roleService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.setting.seed.ts b/src/migration/seeds/migration.setting.seed.ts new file mode 100644 index 0000000..704edd2 --- /dev/null +++ b/src/migration/seeds/migration.setting.seed.ts @@ -0,0 +1,42 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; + +@Injectable() +export class MigrationSettingSeed { + constructor(private readonly settingService: SettingService) {} + + @Command({ + command: 'seed:setting', + describe: 'seeds settings', + }) + async seeds(): Promise { + try { + await this.settingService.create({ + name: 'maintenance', + description: 'Maintenance Mode', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + value: 'false', + }); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:setting', + describe: 'remove settings', + }) + async remove(): Promise { + try { + await this.settingService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.user.seed.ts b/src/migration/seeds/migration.user.seed.ts new file mode 100644 index 0000000..5acdfac --- /dev/null +++ b/src/migration/seeds/migration.user.seed.ts @@ -0,0 +1,97 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { UserService } from 'src/modules/user/services/user.service'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class MigrationUserSeed { + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + private readonly roleService: RoleService + ) {} + + @Command({ + command: 'seed:user', + describe: 'seed users', + }) + async seeds(): Promise { + const password = 'aaAA@@123444'; + const superadminRole: RoleDoc = await this.roleService.findOne({ + name: 'superadmin', + }); + const adminRole: RoleDoc = await this.roleService.findOne({ + name: 'admin', + }); + const userRole: RoleDoc = await this.roleService.findOne({ + name: 'user', + }); + const passwordHash = await this.authService.createPassword( + 'aaAA@@123444' + ); + + const user1: Promise = this.userService.create( + { + username: 'superadmin', + firstName: 'superadmin', + lastName: 'test', + email: 'superadmin@mail.com', + password, + mobileNumber: '08111111222', + role: superadminRole._id, + }, + passwordHash + ); + + const user2: Promise = this.userService.create( + { + username: 'admin', + firstName: 'admin', + lastName: 'test', + email: 'admin@mail.com', + password, + mobileNumber: '08111111111', + role: adminRole._id, + }, + passwordHash + ); + + const user3: Promise = this.userService.create( + { + username: 'user', + firstName: 'user', + lastName: 'test', + email: 'user@mail.com', + password, + mobileNumber: '08111111333', + role: userRole._id, + }, + passwordHash + ); + + try { + await Promise.all([user1, user2, user3]); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:user', + describe: 'remove users', + }) + async remove(): Promise { + try { + await this.userService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/modules/permission/constants/permission.constant.ts b/src/modules/permission/constants/permission.constant.ts new file mode 100644 index 0000000..1d6e8f5 --- /dev/null +++ b/src/modules/permission/constants/permission.constant.ts @@ -0,0 +1 @@ +export const PERMISSION_ACTIVE_META_KEY = 'PermissionActiveMetaKey'; diff --git a/src/modules/permission/constants/permission.doc.constant.ts b/src/modules/permission/constants/permission.doc.constant.ts new file mode 100644 index 0000000..2c65d93 --- /dev/null +++ b/src/modules/permission/constants/permission.doc.constant.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PermissionDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const PermissionDocQueryGroup = [ + { + name: 'group', + allowEmptyValue: false, + required: true, + type: 'string', + example: `${ENUM_PERMISSION_GROUP.PERMISSION},${ENUM_PERMISSION_GROUP.ROLE}`, + description: "group permissions value with ',' delimiter", + }, +]; + +export const PermissionDocParamsGet = [ + { + name: 'permissions', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/permission/constants/permission.enum.constant.ts b/src/modules/permission/constants/permission.enum.constant.ts new file mode 100644 index 0000000..f9b8c4d --- /dev/null +++ b/src/modules/permission/constants/permission.enum.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_PERMISSION_GROUP { + USER = 'USER', + ROLE = 'ROLE', + PERMISSION = 'PERMISSION', + API_KEY = 'API_KEY', + SETTING = 'SETTING', +} diff --git a/src/modules/permission/constants/permission.list.constant.ts b/src/modules/permission/constants/permission.list.constant.ts new file mode 100644 index 0000000..0c4b46f --- /dev/null +++ b/src/modules/permission/constants/permission.list.constant.ts @@ -0,0 +1,15 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PERMISSION_DEFAULT_ORDER_BY = 'createdAt'; +export const PERMISSION_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const PERMISSION_DEFAULT_PER_PAGE = 20; +export const PERMISSION_DEFAULT_AVAILABLE_ORDER_BY = [ + 'code', + 'name', + 'createdAt', +]; +export const PERMISSION_DEFAULT_AVAILABLE_SEARCH = ['code', 'name']; +export const PERMISSION_DEFAULT_IS_ACTIVE = [true, false]; +export const PERMISSION_DEFAULT_GROUP = Object.values(ENUM_PERMISSION_GROUP); diff --git a/src/modules/permission/constants/permission.status-code.constant.ts b/src/modules/permission/constants/permission.status-code.constant.ts new file mode 100644 index 0000000..63180a8 --- /dev/null +++ b/src/modules/permission/constants/permission.status-code.constant.ts @@ -0,0 +1,6 @@ +export enum ENUM_PERMISSION_STATUS_CODE_ERROR { + PERMISSION_NOT_FOUND_ERROR = 5200, + PERMISSION_GUARD_INVALID_ERROR = 5201, + PERMISSION_IS_ACTIVE_ERROR = 5202, + PERMISSION_INACTIVE_ERROR = 5203, +} diff --git a/src/modules/permission/controllers/permission.admin.controller.ts b/src/modules/permission/controllers/permission.admin.controller.ts new file mode 100644 index 0000000..dc9db86 --- /dev/null +++ b/src/modules/permission/controllers/permission.admin.controller.ts @@ -0,0 +1,253 @@ +import { + Body, + Controller, + Get, + InternalServerErrorException, + Patch, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, + 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 { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { + PERMISSION_DEFAULT_AVAILABLE_ORDER_BY, + PERMISSION_DEFAULT_AVAILABLE_SEARCH, + PERMISSION_DEFAULT_GROUP, + PERMISSION_DEFAULT_IS_ACTIVE, + PERMISSION_DEFAULT_ORDER_BY, + PERMISSION_DEFAULT_ORDER_DIRECTION, + PERMISSION_DEFAULT_PER_PAGE, +} from 'src/modules/permission/constants/permission.list.constant'; +import { + PermissionGetGuard, + PermissionUpdateActiveGuard, + PermissionUpdateGuard, + PermissionUpdateInactiveGuard, +} from 'src/modules/permission/decorators/permission.admin.decorator'; +import { GetPermission } from 'src/modules/permission/decorators/permission.decorator'; +import { + PermissionActiveDoc, + PermissionGetDoc, + PermissionGroupDoc, + PermissionInactiveDoc, + PermissionListDoc, + PermissionUpdateDoc, +} from 'src/modules/permission/docs/permission.admin.doc'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { PermissionRequestDto } from 'src/modules/permission/dtos/permissions.request.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; +import { PermissionGroupsSerialization } from 'src/modules/permission/serializations/permission.group.serialization'; +import { PermissionListSerialization } from 'src/modules/permission/serializations/permission.list.serialization'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@ApiTags('modules.admin.permission') +@Controller({ + version: '1', + path: '/permission', +}) +export class PermissionAdminController { + constructor( + private readonly paginationService: PaginationService, + private readonly permissionService: PermissionService + ) {} + + @PermissionListDoc() + @ResponsePaging('permission.list', { + serialization: PermissionListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + PERMISSION_DEFAULT_PER_PAGE, + PERMISSION_DEFAULT_ORDER_BY, + PERMISSION_DEFAULT_ORDER_DIRECTION, + PERMISSION_DEFAULT_AVAILABLE_SEARCH, + PERMISSION_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean( + 'isActive', + PERMISSION_DEFAULT_IS_ACTIVE + ) + isActive: Record, + @PaginationQueryFilterInEnum( + 'group', + PERMISSION_DEFAULT_GROUP, + ENUM_PERMISSION_GROUP + ) + group: Record + ): Promise { + const find: Record = { + ...isActive, + ..._search, + ...group, + }; + + const permissions: PermissionEntity[] = + await this.permissionService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + + const total: number = await this.permissionService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: permissions, + }; + } + + @PermissionGroupDoc() + @Response('permission.group', { + serialization: PermissionGroupsSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/group') + async group( + @PaginationQueryFilterInEnum( + 'groups', + PERMISSION_DEFAULT_GROUP, + ENUM_PERMISSION_GROUP + ) + groups: Record + ): Promise { + const permissions: PermissionDoc[] = + await this.permissionService.findAllByGroup(groups); + + const permissionGroups: IPermissionGroup[] = + await this.permissionService.groupingByGroups(permissions); + + return { data: { groups: permissionGroups } }; + } + + @PermissionGetDoc() + @Response('permission.get', { + serialization: PermissionGetSerialization, + }) + @PermissionGetGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/get/:permission') + async get( + @GetPermission(true) permission: PermissionEntity + ): Promise { + return { data: permission }; + } + + @PermissionUpdateDoc() + @Response('permission.update', { + serialization: ResponseIdSerialization, + }) + @PermissionUpdateGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:permission') + async update( + @GetPermission() permission: PermissionDoc, + @Body() body: PermissionUpdateDescriptionDto + ): Promise { + try { + await this.permissionService.updateDescription(permission, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: permission._id }, + }; + } + + @PermissionInactiveDoc() + @Response('permission.inactive') + @PermissionUpdateInactiveGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE, + ENUM_AUTH_PERMISSIONS.PERMISSION_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:permission/inactive') + async inactive(@GetPermission() permission: PermissionDoc): Promise { + try { + await this.permissionService.inactive(permission); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @PermissionActiveDoc() + @Response('permission.active', {}) + @PermissionUpdateActiveGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE, + ENUM_AUTH_PERMISSIONS.PERMISSION_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:permission/active') + async active(@GetPermission() permission: PermissionDoc): Promise { + try { + await this.permissionService.active(permission); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/permission/decorators/permission.admin.decorator.ts b/src/modules/permission/decorators/permission.admin.decorator.ts new file mode 100644 index 0000000..e7a20e4 --- /dev/null +++ b/src/modules/permission/decorators/permission.admin.decorator.ts @@ -0,0 +1,44 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { PERMISSION_ACTIVE_META_KEY } from 'src/modules/permission/constants/permission.constant'; +import { PermissionActiveGuard } from 'src/modules/permission/guards/permission.active.guard'; +import { PermissionNotFoundGuard } from 'src/modules/permission/guards/permission.not-found.guard'; +import { PermissionPutToRequestGuard } from 'src/modules/permission/guards/permission.put-to-request.guard'; + +export function PermissionGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(PermissionPutToRequestGuard, PermissionNotFoundGuard) + ); +} + +export function PermissionUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [true]) + ); +} + +export function PermissionUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [false]) + ); +} + +export function PermissionUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/modules/permission/decorators/permission.decorator.ts b/src/modules/permission/decorators/permission.decorator.ts new file mode 100644 index 0000000..e494789 --- /dev/null +++ b/src/modules/permission/decorators/permission.decorator.ts @@ -0,0 +1,15 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; + +export const GetPermission = createParamDecorator( + ( + returnPlain: boolean, + ctx: ExecutionContext + ): PermissionDoc | PermissionEntity => { + const { __permission } = ctx.switchToHttp().getRequest(); + return returnPlain ? __permission.toObject() : __permission; + } +); diff --git a/src/modules/permission/docs/permission.admin.doc.ts b/src/modules/permission/docs/permission.admin.doc.ts new file mode 100644 index 0000000..8c1029a --- /dev/null +++ b/src/modules/permission/docs/permission.admin.doc.ts @@ -0,0 +1,106 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + PermissionDocParamsGet, + PermissionDocQueryGroup, + PermissionDocQueryIsActive, +} from 'src/modules/permission/constants/permission.doc.constant'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; +import { PermissionGroupsSerialization } from 'src/modules/permission/serializations/permission.group.serialization'; +import { PermissionListSerialization } from 'src/modules/permission/serializations/permission.list.serialization'; + +export function PermissionListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('permission.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [ + ...PermissionDocQueryIsActive, + ...PermissionDocQueryGroup, + ], + }, + response: { + serialization: PermissionListSerialization, + }, + }) + ); +} + +export function PermissionGetDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + response: { serialization: PermissionGetSerialization }, + }) + ); +} + +export function PermissionUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function PermissionActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + }) + ); +} + +export function PermissionInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + }) + ); +} + +export function PermissionGroupDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.group', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: PermissionDocQueryGroup, + }, + response: { + serialization: PermissionGroupsSerialization, + }, + }) + ); +} diff --git a/src/modules/permission/dtos/permission.active.dto.ts b/src/modules/permission/dtos/permission.active.dto.ts new file mode 100644 index 0000000..24918ad --- /dev/null +++ b/src/modules/permission/dtos/permission.active.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class PermissionActiveDto { + @ApiProperty({ + name: 'isActive', + description: 'is active permission', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/modules/permission/dtos/permission.create.dto.ts b/src/modules/permission/dtos/permission.create.dto.ts new file mode 100644 index 0000000..a96789b --- /dev/null +++ b/src/modules/permission/dtos/permission.create.dto.ts @@ -0,0 +1,33 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsEnum } from 'class-validator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class PermissionCreateDto { + @ApiProperty({ + description: 'Permission group', + example: 'PERMISSION', + required: true, + }) + @IsEnum(ENUM_PERMISSION_GROUP) + @IsNotEmpty() + group: ENUM_PERMISSION_GROUP; + + @ApiProperty({ + description: 'Unique code of permission', + example: faker.random.alpha(5), + required: true, + }) + @IsString() + @IsNotEmpty() + code: string; + + @ApiProperty({ + description: 'Description of permission', + example: 'blabla description', + required: true, + }) + @IsString() + @IsNotEmpty() + description: string; +} diff --git a/src/modules/permission/dtos/permission.update-description.dto.ts b/src/modules/permission/dtos/permission.update-description.dto.ts new file mode 100644 index 0000000..1f43ff6 --- /dev/null +++ b/src/modules/permission/dtos/permission.update-description.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { PermissionCreateDto } from './permission.create.dto'; + +export class PermissionUpdateDescriptionDto extends PickType( + PermissionCreateDto, + ['description'] as const +) {} diff --git a/src/modules/permission/dtos/permission.update-group.dto.ts b/src/modules/permission/dtos/permission.update-group.dto.ts new file mode 100644 index 0000000..c79ca28 --- /dev/null +++ b/src/modules/permission/dtos/permission.update-group.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; + +export class PermissionUpdateGroupDto extends PickType(PermissionCreateDto, [ + 'group', +] as const) {} diff --git a/src/modules/permission/dtos/permissions.request.dto.ts b/src/modules/permission/dtos/permissions.request.dto.ts new file mode 100644 index 0000000..c6a5da8 --- /dev/null +++ b/src/modules/permission/dtos/permissions.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class PermissionRequestDto { + @ApiProperty({ + name: 'permission', + description: 'permission id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + permission: string; +} diff --git a/src/modules/permission/guards/permission.active.guard.ts b/src/modules/permission/guards/permission.active.guard.ts new file mode 100644 index 0000000..1aae130 --- /dev/null +++ b/src/modules/permission/guards/permission.active.guard.ts @@ -0,0 +1,36 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PERMISSION_ACTIVE_META_KEY } from 'src/modules/permission/constants/permission.constant'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; + +@Injectable() +export class PermissionActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + PERMISSION_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __permission } = context.switchToHttp().getRequest(); + + if (!required.includes(__permission.isActive)) { + throw new BadRequestException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR, + message: 'permission.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/permission/guards/permission.not-found.guard.ts b/src/modules/permission/guards/permission.not-found.guard.ts new file mode 100644 index 0000000..3c90903 --- /dev/null +++ b/src/modules/permission/guards/permission.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; + +@Injectable() +export class PermissionNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __permission } = context.switchToHttp().getRequest(); + + if (!__permission) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + return true; + } +} diff --git a/src/modules/permission/guards/permission.put-to-request.guard.ts b/src/modules/permission/guards/permission.put-to-request.guard.ts new file mode 100644 index 0000000..e4dd08a --- /dev/null +++ b/src/modules/permission/guards/permission.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { PermissionDoc } from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@Injectable() +export class PermissionPutToRequestGuard implements CanActivate { + constructor(private readonly permissionService: PermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { permission } = params; + + const check: PermissionDoc = await this.permissionService.findOneById( + permission + ); + request.__permission = check; + + return true; + } +} diff --git a/src/modules/permission/interfaces/permission.interface.ts b/src/modules/permission/interfaces/permission.interface.ts new file mode 100644 index 0000000..851d0c7 --- /dev/null +++ b/src/modules/permission/interfaces/permission.interface.ts @@ -0,0 +1,7 @@ +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; + +export interface IPermissionGroup { + group: ENUM_PERMISSION_GROUP; + permissions: PermissionEntity[]; +} diff --git a/src/modules/permission/interfaces/permission.service.interface.ts b/src/modules/permission/interfaces/permission.service.interface.ts new file mode 100644 index 0000000..92aa096 --- /dev/null +++ b/src/modules/permission/interfaces/permission.service.interface.ts @@ -0,0 +1,85 @@ +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { PermissionUpdateGroupDto } from 'src/modules/permission/dtos/permission.update-group.dto'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { ENUM_PERMISSION_GROUP } from "../constants/permission.enum.constant"; + +export interface IPermissionService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findAllByIds( + ids: string[], + options?: IDatabaseFindAllOptions + ): Promise; + + findAllByGroup( + filterGroups?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + delete(repository: PermissionDoc): Promise; + + create( + { group, code, description }: PermissionCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + updateDescription( + repository: PermissionDoc, + { description }: PermissionUpdateDescriptionDto + ): Promise; + + updateGroup( + repository: PermissionDoc, + data: PermissionUpdateGroupDto + ): Promise; + + active(repository: PermissionDoc): Promise; + + inactive(repository: PermissionDoc): Promise; + + groupingByGroups( + permissions: PermissionDoc[], + scope?: ENUM_PERMISSION_GROUP[] + ): Promise; + + createMany( + data: PermissionCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/modules/permission/permission.module.ts b/src/modules/permission/permission.module.ts new file mode 100644 index 0000000..d440724 --- /dev/null +++ b/src/modules/permission/permission.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PermissionRepositoryModule } from 'src/modules/permission/repository/permission.repository.module'; +import { PermissionService } from './services/permission.service'; + +@Module({ + controllers: [], + providers: [PermissionService], + exports: [PermissionService], + imports: [PermissionRepositoryModule], +}) +export class PermissionModule {} diff --git a/src/modules/permission/repository/entities/permission.entity.ts b/src/modules/permission/repository/entities/permission.entity.ts new file mode 100644 index 0000000..643eeb2 --- /dev/null +++ b/src/modules/permission/repository/entities/permission.entity.ts @@ -0,0 +1,58 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PermissionDatabaseName = 'permissions'; + +@DatabaseEntity({ collection: PermissionDatabaseName }) +export class PermissionEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + uppercase: true, + trim: true, + maxlength: 25, + type: String, + }) + code: string; + + @Prop({ + required: true, + index: true, + trim: true, + enum: ENUM_PERMISSION_GROUP, + type: String, + }) + group: ENUM_PERMISSION_GROUP; + + @Prop({ + required: true, + type: String, + maxlength: 255, + }) + description: string; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; +} + +export const PermissionSchema = SchemaFactory.createForClass(PermissionEntity); + +export type PermissionDoc = PermissionEntity & Document; + +PermissionSchema.pre( + 'save', + function (next: CallbackWithoutResultAndOptionalError) { + this.code = this.code.toUpperCase(); + + next(); + } +); diff --git a/src/modules/permission/repository/permission.repository.module.ts b/src/modules/permission/repository/permission.repository.module.ts new file mode 100644 index 0000000..f475a31 --- /dev/null +++ b/src/modules/permission/repository/permission.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + PermissionEntity, + PermissionSchema, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionRepository } from 'src/modules/permission/repository/repositories/permission.repository'; + +@Module({ + providers: [PermissionRepository], + exports: [PermissionRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: PermissionEntity.name, + schema: PermissionSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class PermissionRepositoryModule {} diff --git a/src/modules/permission/repository/repositories/permission.repository.ts b/src/modules/permission/repository/repositories/permission.repository.ts new file mode 100644 index 0000000..e59c92d --- /dev/null +++ b/src/modules/permission/repository/repositories/permission.repository.ts @@ -0,0 +1,21 @@ +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 { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; + +@Injectable() +export class PermissionRepository extends DatabaseMongoUUIDRepositoryAbstract< + PermissionEntity, + PermissionDoc +> { + constructor( + @DatabaseModel(PermissionEntity.name) + private readonly permissionModel: Model + ) { + super(permissionModel); + } +} diff --git a/src/modules/permission/serializations/permission.get.serialization.ts b/src/modules/permission/serializations/permission.get.serialization.ts new file mode 100644 index 0000000..21e7b5c --- /dev/null +++ b/src/modules/permission/serializations/permission.get.serialization.ts @@ -0,0 +1,51 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class PermissionGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Active flag of permission', + example: true, + required: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + description: 'Unique code of permission', + example: faker.random.alpha(5), + required: true, + }) + readonly code: string; + + @ApiProperty({ + enum: ENUM_PERMISSION_GROUP, + type: 'string', + }) + readonly group: ENUM_PERMISSION_GROUP; + + @ApiProperty({ + description: 'Description of permission', + example: 'blabla description', + required: false, + }) + readonly description?: string; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/permission/serializations/permission.group.serialization.ts b/src/modules/permission/serializations/permission.group.serialization.ts new file mode 100644 index 0000000..3b039b8 --- /dev/null +++ b/src/modules/permission/serializations/permission.group.serialization.ts @@ -0,0 +1,35 @@ +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; + +class PermissionGroupPermissionSerialization extends OmitType( + PermissionGetSerialization, + ['group', 'createdAt', 'updatedAt'] as const +) { + @Exclude() + group: Date; + + @Exclude() + createdAt: Date; + + @Exclude() + updatedAt: Date; +} + +class PermissionGroupSerialization extends PickType( + PermissionGetSerialization, + ['group'] as const +) { + @ApiProperty({ + type: () => PermissionGroupPermissionSerialization, + isArray: true, + }) + @Type(() => PermissionGroupPermissionSerialization) + permissions: PermissionGroupPermissionSerialization[]; +} + +export class PermissionGroupsSerialization { + @ApiProperty({ type: () => PermissionGroupSerialization, isArray: true }) + @Type(() => PermissionGroupSerialization) + groups: PermissionGroupSerialization[]; +} diff --git a/src/modules/permission/serializations/permission.list.serialization.ts b/src/modules/permission/serializations/permission.list.serialization.ts new file mode 100644 index 0000000..a63c64e --- /dev/null +++ b/src/modules/permission/serializations/permission.list.serialization.ts @@ -0,0 +1,3 @@ +import { PermissionGetSerialization } from './permission.get.serialization'; + +export class PermissionListSerialization extends PermissionGetSerialization {} diff --git a/src/modules/permission/services/permission.service.ts b/src/modules/permission/services/permission.service.ts new file mode 100644 index 0000000..0815373 --- /dev/null +++ b/src/modules/permission/services/permission.service.ts @@ -0,0 +1,171 @@ +import { Injectable } from '@nestjs/common'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { PermissionUpdateGroupDto } from 'src/modules/permission/dtos/permission.update-group.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { IPermissionService } from 'src/modules/permission/interfaces/permission.service.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionRepository } from 'src/modules/permission/repository/repositories/permission.repository'; + +@Injectable() +export class PermissionService implements IPermissionService { + constructor(private readonly permissionRepository: PermissionRepository) {} + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll(find, { + ...options + }); + } + + async findAllByIds( + ids: string[], + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll( + { _id: { $in: ids } }, + options + ); + } + + async findAllByGroup( + filterGroups?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll( + { ...filterGroups }, + options + ); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.permissionRepository.findOneById(_id, { + ...options, + }); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.permissionRepository.findOne(find, { + ...options, + }); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.permissionRepository.getTotal(find, options); + } + + async delete(repository: PermissionDoc): Promise { + return this.permissionRepository.softDelete(repository); + } + + async create( + { group, code, description }: PermissionCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: PermissionEntity = new PermissionEntity(); + create.group = group; + create.code = code; + create.description = description ?? undefined; + create.isActive = true; + + return this.permissionRepository.create( + create, + options + ); + } + + async updateDescription( + repository: PermissionDoc, + { description }: PermissionUpdateDescriptionDto + ): Promise { + repository.description = description; + + return this.permissionRepository.save(repository); + } + + async updateGroup( + repository: PermissionDoc, + { group }: PermissionUpdateGroupDto + ): Promise { + repository.group = group; + + return this.permissionRepository.save(repository); + } + + async active(repository: PermissionDoc): Promise { + repository.isActive = true; + return this.permissionRepository.save(repository); + } + + async inactive(repository: PermissionDoc): Promise { + repository.isActive = false; + + return this.permissionRepository.save(repository); + } + + async groupingByGroups( + permissions: PermissionDoc[], + scope?: ENUM_PERMISSION_GROUP[] + ): Promise { + const permissionGroups: ENUM_PERMISSION_GROUP[] = + scope ??Object.values(ENUM_PERMISSION_GROUP); + const permissionEntity: PermissionEntity[] = permissions + .map((val) => val.toObject()) + .filter((val) => permissionGroups.includes(val.group)); + return permissionGroups.map((val) => ({ + group: val, + permissions: permissionEntity.filter((l) => l.group === val), + })); + } + + async createMany( + data: PermissionCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const create: PermissionEntity[] = data.map((val) => { + const entity: PermissionEntity = new PermissionEntity(); + entity.code = val.code; + entity.description = val?.description; + entity.group = val.group; + entity.isActive = true; + + return entity; + }) as PermissionEntity[]; + + return this.permissionRepository.createMany( + create, + options + ); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.permissionRepository.deleteMany(find, options); + } +} diff --git a/src/modules/role/constants/role.constant.ts b/src/modules/role/constants/role.constant.ts new file mode 100644 index 0000000..59d558e --- /dev/null +++ b/src/modules/role/constants/role.constant.ts @@ -0,0 +1 @@ +export const ROLE_ACTIVE_META_KEY = 'RoleActiveMetaKey'; diff --git a/src/modules/role/constants/role.doc.constant.ts b/src/modules/role/constants/role.doc.constant.ts new file mode 100644 index 0000000..84d082a --- /dev/null +++ b/src/modules/role/constants/role.doc.constant.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export const RoleDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const RoleDocQueryAccessFor = [ + { + name: 'accessFor', + allowEmptyValue: false, + required: true, + type: 'string', + example: Object.values(ENUM_AUTH_ACCESS_FOR).join(','), + description: "enum value with ',' delimiter", + }, +]; + +export const RoleDocParamsGet = [ + { + name: 'role', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/role/constants/role.list.constant.ts b/src/modules/role/constants/role.list.constant.ts new file mode 100644 index 0000000..35846b9 --- /dev/null +++ b/src/modules/role/constants/role.list.constant.ts @@ -0,0 +1,11 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const ROLE_DEFAULT_ORDER_BY = 'createdAt'; +export const ROLE_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const ROLE_DEFAULT_PER_PAGE = 20; +export const ROLE_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'createdAt']; +export const ROLE_DEFAULT_AVAILABLE_SEARCH = ['name']; +export const ROLE_DEFAULT_IS_ACTIVE = [true, false]; +export const ROLE_DEFAULT_ACCESS_FOR = Object.values(ENUM_AUTH_ACCESS_FOR); diff --git a/src/modules/role/constants/role.status-code.constant.ts b/src/modules/role/constants/role.status-code.constant.ts new file mode 100644 index 0000000..1a17eb5 --- /dev/null +++ b/src/modules/role/constants/role.status-code.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_ROLE_STATUS_CODE_ERROR { + ROLE_NOT_FOUND_ERROR = 5500, + ROLE_EXIST_ERROR = 5501, + ROLE_IS_ACTIVE_ERROR = 5502, + ROLE_INACTIVE_ERROR = 5503, + ROLE_USED_ERROR = 5504, +} diff --git a/src/modules/role/controllers/role.admin.controller.ts b/src/modules/role/controllers/role.admin.controller.ts new file mode 100644 index 0000000..af48caa --- /dev/null +++ b/src/modules/role/controllers/role.admin.controller.ts @@ -0,0 +1,381 @@ +import { + Body, + ConflictException, + Controller, + Delete, + Get, + InternalServerErrorException, + NotFoundException, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, + 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 { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { PermissionDoc, PermissionEntity } from "src/modules/permission/repository/entities/permission.entity"; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { + ROLE_DEFAULT_ACCESS_FOR, + ROLE_DEFAULT_AVAILABLE_ORDER_BY, + ROLE_DEFAULT_AVAILABLE_SEARCH, + ROLE_DEFAULT_IS_ACTIVE, + ROLE_DEFAULT_ORDER_BY, + ROLE_DEFAULT_ORDER_DIRECTION, + ROLE_DEFAULT_PER_PAGE, +} from 'src/modules/role/constants/role.list.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { + RoleDeleteGuard, + RoleGetGuard, + RoleUpdateActiveGuard, + RoleUpdateGuard, + RoleUpdateInactiveGuard, +} from 'src/modules/role/decorators/role.admin.decorator'; +import { GetRole } from 'src/modules/role/decorators/role.decorator'; +import { + RoleAccessForDoc, + RoleActiveDoc, + RoleCreateDoc, + RoleDeleteDoc, + RoleGetDoc, + RoleInactiveDoc, + RoleListDoc, + RoleUpdateDoc, +} from 'src/modules/role/docs/role.admin.doc'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleRequestDto } from 'src/modules/role/dtos/role.request.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleAccessForSerialization } from 'src/modules/role/serializations/role.access-for.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; +import { RoleListSerialization } from 'src/modules/role/serializations/role.list.serialization'; +import { RoleService } from 'src/modules/role/services/role.service'; + +@ApiTags('modules.admin.role') +@Controller({ + version: '1', + path: '/role', +}) +export class RoleAdminController { + constructor( + private readonly paginationService: PaginationService, + private readonly permissionService: PermissionService, + private readonly roleService: RoleService + ) {} + + @RoleListDoc() + @ResponsePaging('role.list', { + serialization: RoleListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + ROLE_DEFAULT_PER_PAGE, + ROLE_DEFAULT_ORDER_BY, + ROLE_DEFAULT_ORDER_DIRECTION, + ROLE_DEFAULT_AVAILABLE_SEARCH, + ROLE_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', ROLE_DEFAULT_IS_ACTIVE) + isActive: Record, + @PaginationQueryFilterInEnum( + 'accessFor', + ROLE_DEFAULT_ACCESS_FOR, + ENUM_AUTH_ACCESS_FOR + ) + accessFor: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + ...accessFor, + }; + + const roles: RoleEntity[] = 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 + ); + + return { + _pagination: { total, totalPage }, + data: roles, + }; + } + + @RoleGetDoc() + @Response('role.get', { + serialization: RoleGetSerialization, + }) + @RoleGetGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('get/:role') + async get(@GetRole(true) role: RoleEntity): Promise { + const permissions: PermissionEntity[] = + await this.permissionService.findAllByIds(role.permissions); + return { data: { ...role, permissions } }; + } + + @RoleCreateDoc() + @Response('role.create', { + serialization: ResponseIdSerialization, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_CREATE + ) + @AuthJwtAdminAccessProtected() + @Post('/create') + async create( + @Body() + { name, permissions, accessFor }: RoleCreateDto + ): Promise { + const exist: boolean = await this.roleService.existByName(name); + if (exist) { + throw new ConflictException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR, + message: 'role.error.exist', + }); + } + + const permissionsCheck: PermissionDoc[] = + await this.permissionService.findAllByIds(permissions); + if (permissionsCheck.length !== permissions.length) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + + try { + const create = await this.roleService.create({ + name, + permissions, + accessFor, + }); + + return { + data: { _id: create._id }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @RoleUpdateDoc() + @Response('role.update', { + serialization: ResponseIdSerialization, + }) + @RoleUpdateGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:role') + async update( + @GetRole() role: RoleDoc, + @Body() + { name }: RoleUpdateNameDto + ): Promise { + const check: boolean = await this.roleService.existByName(name, { + excludeId: [role._id], + }); + if (check) { + throw new ConflictException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR, + message: 'role.error.exist', + }); + } + + try { + await this.roleService.updateName(role, { name }); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: role._id }, + }; + } + + @RoleUpdateDoc() + @Response('role.updatePermission', { + serialization: ResponseIdSerialization, + }) + @RoleUpdateGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:role/permission') + async updatePermission( + @GetRole() role: RoleDoc, + @Body() + { accessFor, permissions }: RoleUpdatePermissionDto + ): Promise { + const permissionsCheck: PermissionDoc[] = + await this.permissionService.findAllByIds(permissions); + + if (permissionsCheck.length !== permissions.length) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + + try { + await this.roleService.updatePermission(role, { + accessFor, + permissions, + }); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: role._id }, + }; + } + + @RoleDeleteDoc() + @Response('role.delete') + @RoleDeleteGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_DELETE + ) + @AuthJwtAdminAccessProtected() + @Delete('/delete/:role') + async delete(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.delete(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleInactiveDoc() + @Response('role.inactive') + @RoleUpdateInactiveGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:role/inactive') + async inactive(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.inactive(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleActiveDoc() + @Response('role.active') + @RoleUpdateActiveGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:role/active') + async active(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.active(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleAccessForDoc() + @Response('role.accessFor', { serialization: RoleAccessForSerialization }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('/access-for') + async accessFor(): Promise { + const accessFor: string[] = await this.roleService.getAccessFor(); + + return { data: { accessFor } }; + } +} diff --git a/src/modules/role/decorators/role.admin.decorator.ts b/src/modules/role/decorators/role.admin.decorator.ts new file mode 100644 index 0000000..9572403 --- /dev/null +++ b/src/modules/role/decorators/role.admin.decorator.ts @@ -0,0 +1,48 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { ROLE_ACTIVE_META_KEY } from 'src/modules/role/constants/role.constant'; +import { RoleActiveGuard } from 'src/modules/role/guards/role.active.guard'; +import { RoleNotFoundGuard } from 'src/modules/role/guards/role.not-found.guard'; +import { RolePutToRequestGuard } from 'src/modules/role/guards/role.put-to-request.guard'; +import { RoleUsedGuard } from 'src/modules/role/guards/role.used.guard'; + +export function RoleGetGuard(): MethodDecorator { + return applyDecorators(UseGuards(RolePutToRequestGuard, RoleNotFoundGuard)); +} + +export function RoleUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + RolePutToRequestGuard, + RoleNotFoundGuard, + RoleActiveGuard, + RoleUsedGuard + ), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} + +export function RoleDeleteGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + RolePutToRequestGuard, + RoleNotFoundGuard, + RoleActiveGuard, + RoleUsedGuard + ), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} + +export function RoleUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(RolePutToRequestGuard, RoleNotFoundGuard, RoleActiveGuard), + SetMetadata(ROLE_ACTIVE_META_KEY, [false]) + ); +} + +export function RoleUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(RolePutToRequestGuard, RoleNotFoundGuard, RoleActiveGuard), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/modules/role/decorators/role.decorator.ts b/src/modules/role/decorators/role.decorator.ts new file mode 100644 index 0000000..260b4b1 --- /dev/null +++ b/src/modules/role/decorators/role.decorator.ts @@ -0,0 +1,12 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +export const GetRole = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): RoleDoc | RoleEntity => { + const { __role } = ctx.switchToHttp().getRequest(); + return returnPlain ? __role.toObject() : __role; + } +); diff --git a/src/modules/role/docs/role.admin.doc.ts b/src/modules/role/docs/role.admin.doc.ts new file mode 100644 index 0000000..317d008 --- /dev/null +++ b/src/modules/role/docs/role.admin.doc.ts @@ -0,0 +1,127 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + RoleDocParamsGet, + RoleDocQueryAccessFor, + RoleDocQueryIsActive, +} from 'src/modules/role/constants/role.doc.constant'; +import { RoleAccessForSerialization } from 'src/modules/role/serializations/role.access-for.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; +import { RoleListSerialization } from 'src/modules/role/serializations/role.list.serialization'; + +export function RoleListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('role.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [...RoleDocQueryIsActive, ...RoleDocQueryAccessFor], + }, + response: { + serialization: RoleListSerialization, + }, + }) + ); +} + +export function RoleGetDoc(): MethodDecorator { + return applyDecorators( + Doc('role.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + response: { serialization: RoleGetSerialization }, + }) + ); +} + +export function RoleCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('role.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ResponseIdSerialization, + }, + }) + ); +} + +export function RoleUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('role.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function RoleDeleteDoc(): MethodDecorator { + return applyDecorators( + Doc('role.delete', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('role.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('role.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleAccessForDoc(): MethodDecorator { + return applyDecorators( + Doc('role.accessFor', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { serialization: RoleAccessForSerialization }, + }) + ); +} diff --git a/src/modules/role/dtos/role.active.dto.ts b/src/modules/role/dtos/role.active.dto.ts new file mode 100644 index 0000000..dfcf6f7 --- /dev/null +++ b/src/modules/role/dtos/role.active.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class RoleActiveDto { + @ApiProperty({ + name: 'isActive', + description: 'is active role', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/modules/role/dtos/role.create.dto.ts b/src/modules/role/dtos/role.create.dto.ts new file mode 100644 index 0000000..8d40fef --- /dev/null +++ b/src/modules/role/dtos/role.create.dto.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + MaxLength, + MinLength, + IsEnum, + IsArray, + IsUUID, +} from 'class-validator'; +import { ENUM_AUTH_ACCESS_FOR_DEFAULT } from 'src/common/auth/constants/auth.enum.constant'; + +export class RoleCreateDto { + @ApiProperty({ + description: 'Alias name of role', + example: faker.name.jobTitle(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(3) + @MaxLength(30) + @Type(() => String) + readonly name: string; + + @ApiProperty({ + description: 'List of permission', + example: [faker.datatype.uuid(), faker.datatype.uuid()], + required: true, + }) + @IsUUID('4', { each: true }) + @IsArray() + @IsNotEmpty() + readonly permissions: string[]; + + @ApiProperty({ + description: 'Representative for role', + example: 'ADMIN', + required: true, + }) + @IsEnum(ENUM_AUTH_ACCESS_FOR_DEFAULT) + @IsNotEmpty() + readonly accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT; +} diff --git a/src/modules/role/dtos/role.request.dto.ts b/src/modules/role/dtos/role.request.dto.ts new file mode 100644 index 0000000..2c9eb6f --- /dev/null +++ b/src/modules/role/dtos/role.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class RoleRequestDto { + @ApiProperty({ + name: 'role', + description: 'role id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + role: string; +} diff --git a/src/modules/role/dtos/role.update-name.dto.ts b/src/modules/role/dtos/role.update-name.dto.ts new file mode 100644 index 0000000..a9b56da --- /dev/null +++ b/src/modules/role/dtos/role.update-name.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { RoleCreateDto } from './role.create.dto'; + +export class RoleUpdateNameDto extends PickType(RoleCreateDto, [ + 'name', +] as const) {} diff --git a/src/modules/role/dtos/role.update-permission.dto.ts b/src/modules/role/dtos/role.update-permission.dto.ts new file mode 100644 index 0000000..abbe963 --- /dev/null +++ b/src/modules/role/dtos/role.update-permission.dto.ts @@ -0,0 +1,7 @@ +import { RoleCreateDto } from './role.create.dto'; +import { PickType } from '@nestjs/swagger'; + +export class RoleUpdatePermissionDto extends PickType(RoleCreateDto, [ + 'accessFor', + 'permissions', +] as const) {} diff --git a/src/modules/role/guards/role.active.guard.ts b/src/modules/role/guards/role.active.guard.ts new file mode 100644 index 0000000..f326af8 --- /dev/null +++ b/src/modules/role/guards/role.active.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLE_ACTIVE_META_KEY } from 'src/modules/role/constants/role.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; + +@Injectable() +export class RoleActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + ROLE_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __role } = context.switchToHttp().getRequest(); + + if (!required.includes(__role.isActive)) { + throw new BadRequestException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR, + message: 'role.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/role/guards/role.not-found.guard.ts b/src/modules/role/guards/role.not-found.guard.ts new file mode 100644 index 0000000..fc67b02 --- /dev/null +++ b/src/modules/role/guards/role.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; + +@Injectable() +export class RoleNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __role } = context.switchToHttp().getRequest(); + + if (!__role) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR, + message: 'role.error.notFound', + }); + } + + return true; + } +} diff --git a/src/modules/role/guards/role.put-to-request.guard.ts b/src/modules/role/guards/role.put-to-request.guard.ts new file mode 100644 index 0000000..9098aa0 --- /dev/null +++ b/src/modules/role/guards/role.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleService } from 'src/modules/role/services/role.service'; + +@Injectable() +export class RolePutToRequestGuard implements CanActivate { + constructor(private readonly roleService: RoleService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { role } = params; + + const check: RoleDoc = await this.roleService.findOneById(role, { + join: true, + }); + request.__role = check; + + return true; + } +} diff --git a/src/modules/role/guards/role.used.guard.ts b/src/modules/role/guards/role.used.guard.ts new file mode 100644 index 0000000..613a087 --- /dev/null +++ b/src/modules/role/guards/role.used.guard.ts @@ -0,0 +1,32 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { UserDoc } from "src/modules/user/repository/entities/user.entity"; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class RoleUsedGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const { __role } = context.switchToHttp().getRequest(); + const check: UserDoc = await this.userService.findOne( + { + role: __role._id, + } + ); + + if (check) { + throw new BadRequestException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_USED_ERROR, + message: 'role.error.used', + }); + } + + return true; + } +} diff --git a/src/modules/role/interfaces/role.interface.ts b/src/modules/role/interfaces/role.interface.ts new file mode 100644 index 0000000..9fb70c2 --- /dev/null +++ b/src/modules/role/interfaces/role.interface.ts @@ -0,0 +1,16 @@ +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +export interface IRoleEntity extends Omit { + permissions: PermissionEntity[]; +} + +export interface IRoleDoc extends Omit { + permissions: PermissionDoc[]; +} diff --git a/src/modules/role/interfaces/role.service.interface.ts b/src/modules/role/interfaces/role.service.interface.ts new file mode 100644 index 0000000..8203757 --- /dev/null +++ b/src/modules/role/interfaces/role.service.interface.ts @@ -0,0 +1,87 @@ +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseCreateManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { IRoleDoc } from "./role.interface"; + +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?: IDatabaseOptions + ): Promise; + + exist(_id: string, options?: IDatabaseExistOptions): Promise; + + existByName( + name: string, + options?: IDatabaseExistOptions + ): Promise; + + create( + {accessFor, permissions, name}: RoleCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + createSuperAdmin(options?: IDatabaseCreateOptions): Promise; + + updateName( + repository: RoleDoc, + {name}: RoleUpdateNameDto + ): Promise; + + updatePermission( + repository: RoleDoc, + {accessFor, permissions}: RoleUpdatePermissionDto + ): Promise; + + active(repository: RoleDoc): Promise; + + inactive(repository: RoleDoc): Promise; + + joinWithPermission(repository: RoleDoc): Promise; + + delete(repository: RoleDoc): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; + + createMany( + data: RoleCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise; + + getAccessFor(): Promise; +} diff --git a/src/modules/role/repository/entities/role.entity.ts b/src/modules/role/repository/entities/role.entity.ts new file mode 100644 index 0000000..744c0e5 --- /dev/null +++ b/src/modules/role/repository/entities/role.entity.ts @@ -0,0 +1,57 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; + +export const RoleDatabaseName = 'roles'; + +@DatabaseEntity({ collection: RoleDatabaseName }) +export class RoleEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + lowercase: true, + trim: true, + maxlength: 100, + type: String, + }) + name: string; + + @Prop({ + required: true, + default: [], + _id: false, + type: Array, + ref: PermissionEntity.name, + }) + permissions: string[]; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: true, + enum: ENUM_AUTH_ACCESS_FOR, + index: true, + type: String, + }) + accessFor: ENUM_AUTH_ACCESS_FOR; +} + +export const RoleSchema = SchemaFactory.createForClass(RoleEntity); + +export type RoleDoc = RoleEntity & Document; + +RoleSchema.pre('save', function (next: CallbackWithoutResultAndOptionalError) { + this.name = this.name.toLowerCase(); + + next(); +}); diff --git a/src/modules/role/repository/repositories/role.repository.ts b/src/modules/role/repository/repositories/role.repository.ts new file mode 100644 index 0000000..0dcd331 --- /dev/null +++ b/src/modules/role/repository/repositories/role.repository.ts @@ -0,0 +1,27 @@ +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 { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +@Injectable() +export class RoleRepository extends DatabaseMongoUUIDRepositoryAbstract< + RoleEntity, + RoleDoc +> { + constructor( + @DatabaseModel(RoleEntity.name) + private readonly roleModel: Model + ) { + super(roleModel, { + path: 'permissions', + localField: 'permissions', + foreignField: '_id', + model: PermissionEntity.name, + }); + } +} diff --git a/src/modules/role/repository/role.repository.module.ts b/src/modules/role/repository/role.repository.module.ts new file mode 100644 index 0000000..169e73b --- /dev/null +++ b/src/modules/role/repository/role.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + RoleEntity, + RoleSchema, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleRepository } from 'src/modules/role/repository/repositories/role.repository'; + +@Module({ + providers: [RoleRepository], + exports: [RoleRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: RoleEntity.name, + schema: RoleSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class RoleRepositoryModule {} diff --git a/src/modules/role/role.module.ts b/src/modules/role/role.module.ts new file mode 100644 index 0000000..b275938 --- /dev/null +++ b/src/modules/role/role.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { RoleRepositoryModule } from 'src/modules/role/repository/role.repository.module'; +import { RoleService } from './services/role.service'; + +@Module({ + controllers: [], + providers: [RoleService], + exports: [RoleService], + imports: [RoleRepositoryModule], +}) +export class RoleModule {} diff --git a/src/modules/role/serializations/role.access-for.serialization.ts b/src/modules/role/serializations/role.access-for.serialization.ts new file mode 100644 index 0000000..236e161 --- /dev/null +++ b/src/modules/role/serializations/role.access-for.serialization.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export class RoleAccessForSerialization { + @ApiProperty({ + description: 'Access for role', + example: [ENUM_AUTH_ACCESS_FOR.USER, ENUM_AUTH_ACCESS_FOR.ADMIN], + required: true, + }) + groups: string[]; +} diff --git a/src/modules/role/serializations/role.get.serialization.ts b/src/modules/role/serializations/role.get.serialization.ts new file mode 100644 index 0000000..c086fcd --- /dev/null +++ b/src/modules/role/serializations/role.get.serialization.ts @@ -0,0 +1,55 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; + +export class RoleGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Active flag of role', + example: true, + required: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + description: 'Alias name of role', + example: faker.name.jobTitle(), + required: true, + }) + readonly name: string; + + @ApiProperty({ + description: 'Representative for role', + example: 'ADMIN', + required: true, + }) + readonly accessFor: ENUM_AUTH_ACCESS_FOR; + + @ApiProperty({ + description: 'List of permission', + type: () => PermissionGetSerialization, + isArray: true, + required: true, + }) + @Type(() => PermissionGetSerialization) + readonly permissions: PermissionGetSerialization[]; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/role/serializations/role.list.serialization.ts b/src/modules/role/serializations/role.list.serialization.ts new file mode 100644 index 0000000..a8afd7d --- /dev/null +++ b/src/modules/role/serializations/role.list.serialization.ts @@ -0,0 +1,16 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { RoleGetSerialization } from './role.get.serialization'; + +export class RoleListSerialization extends OmitType(RoleGetSerialization, [ + 'permissions', +] as const) { + @ApiProperty({ + description: 'Count of permissions', + example: faker.random.numeric(2, { allowLeadingZeros: false }), + required: true, + }) + @Transform(({ value }) => value.length) + readonly permissions: number; +} diff --git a/src/modules/role/services/role.service.ts b/src/modules/role/services/role.service.ts new file mode 100644 index 0000000..6daeca4 --- /dev/null +++ b/src/modules/role/services/role.service.ts @@ -0,0 +1,190 @@ +import { Injectable } from '@nestjs/common'; +import { + ENUM_AUTH_ACCESS_FOR, + ENUM_AUTH_ACCESS_FOR_DEFAULT, +} from 'src/common/auth/constants/auth.enum.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseCreateManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { IRoleService } from 'src/modules/role/interfaces/role.service.interface'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleRepository } from 'src/modules/role/repository/repositories/role.repository'; +import { IRoleDoc } from "../interfaces/role.interface"; + +@Injectable() +export class RoleService implements IRoleService { + constructor(private readonly roleRepository: RoleRepository) {} + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.roleRepository.findAll(find, { + ...options, + join: false, + }); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOne(find, options); + } + + async findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOne({ name }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.roleRepository.getTotal(find, options); + } + + async exist( + _id: string, + options?: IDatabaseExistOptions + ): Promise { + return this.roleRepository.exists( + { + _id, + }, + options + ); + } + + async existByName( + name: string, + options?: IDatabaseExistOptions + ): Promise { + return this.roleRepository.exists( + { + name, + }, + { ...options, withDeleted: true } + ); + } + + async create( + { accessFor, permissions, name }: RoleCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: RoleEntity = new RoleEntity(); + create.accessFor = accessFor; + create.permissions = permissions; + create.isActive = true; + create.name = name; + + return this.roleRepository.create(create, options); + } + + async createSuperAdmin( + options?: IDatabaseCreateOptions + ): Promise { + const create: RoleEntity = new RoleEntity(); + create.name = 'superadmin'; + create.permissions = []; + create.isActive = true; + create.accessFor = ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN; + + return this.roleRepository.create(create, options); + } + + async updateName( + repository: RoleDoc, + { name }: RoleUpdateNameDto + ): Promise { + repository.name = name; + + return this.roleRepository.save(repository); + } + + async joinWithPermission(repository: RoleDoc): Promise { + return repository.populate({ + path: 'permissions', + localField: 'permissions', + foreignField: '_id', + model: PermissionEntity.name, + }); + } + + async updatePermission( + repository: RoleDoc, + { accessFor, permissions }: RoleUpdatePermissionDto + ): Promise { + repository.accessFor = accessFor; + repository.permissions = permissions; + + return this.roleRepository.save(repository); + } + + async active(repository: RoleDoc): Promise { + repository.isActive = true; + + return this.roleRepository.save(repository); + } + + async inactive(repository: RoleDoc): Promise { + repository.isActive = false; + + return this.roleRepository.save(repository); + } + + async delete(repository: RoleDoc): Promise { + return this.roleRepository.softDelete(repository); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.roleRepository.deleteMany(find, options); + } + + async createMany( + data: RoleCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const create: RoleEntity[] = data.map( + ({ accessFor, permissions, name }) => { + const entity: RoleEntity = new RoleEntity(); + entity.accessFor = accessFor; + entity.permissions = permissions; + entity.isActive = true; + entity.name = name; + + return entity; + } + ); + return this.roleRepository.createMany(create, options); + } + + async getAccessFor(): Promise { + return Object.values(ENUM_AUTH_ACCESS_FOR_DEFAULT); + } +} diff --git a/src/modules/user/constants/user.constant.ts b/src/modules/user/constants/user.constant.ts new file mode 100644 index 0000000..cad926c --- /dev/null +++ b/src/modules/user/constants/user.constant.ts @@ -0,0 +1,2 @@ +export const USER_ACTIVE_META_KEY = 'UserActiveMetaKey'; +export const USER_BLOCKED_META_KEY = 'UserBlockedMetaKey'; diff --git a/src/modules/user/constants/user.doc.constant.ts b/src/modules/user/constants/user.doc.constant.ts new file mode 100644 index 0000000..9a71784 --- /dev/null +++ b/src/modules/user/constants/user.doc.constant.ts @@ -0,0 +1,33 @@ +import { faker } from '@faker-js/faker'; + +export const UserDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const UserDocQueryBlocked = [ + { + name: 'blocked', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const UserDocParamsGet = [ + { + name: 'user', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/user/constants/user.list.constant.ts b/src/modules/user/constants/user.list.constant.ts new file mode 100644 index 0000000..964f96b --- /dev/null +++ b/src/modules/user/constants/user.list.constant.ts @@ -0,0 +1,24 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const USER_DEFAULT_PER_PAGE = 20; +export const USER_DEFAULT_ORDER_BY = 'createdAt'; +export const USER_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const USER_DEFAULT_AVAILABLE_ORDER_BY = [ + 'username', + 'firstName', + 'lastName', + 'email', + 'mobileNumber', + 'createdAt', +]; +export const USER_DEFAULT_AVAILABLE_SEARCH = [ + 'username', + 'firstName', + 'lastName', + 'email', + 'mobileNumber', +]; + +export const USER_DEFAULT_IS_ACTIVE = [true, false]; +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 new file mode 100644 index 0000000..4cd69f4 --- /dev/null +++ b/src/modules/user/constants/user.status-code.constant.ts @@ -0,0 +1,13 @@ +export enum ENUM_USER_STATUS_CODE_ERROR { + USER_NOT_FOUND_ERROR = 5400, + USER_USERNAME_EXISTS_ERROR = 5401, + USER_EMAIL_EXIST_ERROR = 5402, + USER_MOBILE_NUMBER_EXIST_ERROR = 5403, + USER_IS_ACTIVE_ERROR = 5404, + USER_INACTIVE_ERROR = 5405, + USER_PASSWORD_NOT_MATCH_ERROR = 5406, + USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR = 5407, + USER_PASSWORD_EXPIRED_ERROR = 5408, + USER_PASSWORD_ATTEMPT_MAX_ERROR = 5409, + USER_BLOCKED_ERROR = 5410, +} diff --git a/src/modules/user/controllers/user.admin.controller.ts b/src/modules/user/controllers/user.admin.controller.ts new file mode 100644 index 0000000..0d10dec --- /dev/null +++ b/src/modules/user/controllers/user.admin.controller.ts @@ -0,0 +1,414 @@ +import { + Controller, + Get, + Post, + Body, + Delete, + Put, + InternalServerErrorException, + NotFoundException, + UploadedFile, + ConflictException, + Patch, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { UploadFileSingle } from 'src/common/file/decorators/file.decorator'; +import { IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { FileExtractPipe } from 'src/common/file/pipes/file.extract.pipe'; +import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; +import { FileSizeExcelPipe } from 'src/common/file/pipes/file.size.pipe'; +import { FileTypeExcelPipe } from 'src/common/file/pipes/file.type.pipe'; +import { FileValidationPipe } from 'src/common/file/pipes/file.validation.pipe'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponseExcel, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +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 { + UserDeleteGuard, + UserGetGuard, + UserUpdateActiveGuard, + UserUpdateBlockedGuard, + UserUpdateGuard, + UserUpdateInactiveGuard, +} from 'src/modules/user/decorators/user.admin.decorator'; +import { GetUser } from 'src/modules/user/decorators/user.decorator'; +import { + UserActiveDoc, + UserBlockedDoc, + UserCreateDoc, + UserDeleteDoc, + UserExportDoc, + UserGetDoc, + UserImportDoc, + UserInactiveDoc, + UserListDoc, + UserUpdateDoc, +} from 'src/modules/user/docs/user.admin.doc'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { UserImportDto } from 'src/modules/user/dtos/user.import.dto'; +import { UserRequestDto } from 'src/modules/user/dtos/user.request.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; +import { UserImportSerialization } from 'src/modules/user/serializations/user.import.serialization'; +import { UserListSerialization } from 'src/modules/user/serializations/user.list.serialization'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { + USER_DEFAULT_AVAILABLE_ORDER_BY, + USER_DEFAULT_AVAILABLE_SEARCH, + USER_DEFAULT_BLOCKED, + USER_DEFAULT_IS_ACTIVE, + USER_DEFAULT_ORDER_BY, + USER_DEFAULT_ORDER_DIRECTION, + USER_DEFAULT_PER_PAGE, +} from 'src/modules/user/constants/user.list.constant'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { IAuthPassword } from "../../../common/auth/interfaces/auth.interface"; + +@ApiTags('modules.admin.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserAdminController { + constructor( + private readonly authService: AuthService, + private readonly paginationService: PaginationService, + private readonly userService: UserService, + private readonly roleService: RoleService + ) {} + + @UserListDoc() + @ResponsePaging('user.list', { + serialization: UserListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.USER_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + USER_DEFAULT_PER_PAGE, + USER_DEFAULT_ORDER_BY, + USER_DEFAULT_ORDER_DIRECTION, + USER_DEFAULT_AVAILABLE_SEARCH, + USER_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', USER_DEFAULT_IS_ACTIVE) + isActive: Record, + @PaginationQueryFilterInBoolean('blocked', USER_DEFAULT_BLOCKED) + blocked: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + ...blocked, + }; + + const users: IUserEntity[] = await this.userService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + const total: number = await this.userService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: users, + }; + } + + @UserGetDoc() + @Response('user.get', { + serialization: UserGetSerialization, + }) + @UserGetGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.USER_READ) + @AuthJwtAdminAccessProtected() + @Get('get/:user') + async get(@GetUser() user: UserDoc): Promise { + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + return { data: userWithRole.toObject() }; + } + + @UserCreateDoc() + @Response('user.create', { + serialization: ResponseIdSerialization, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_CREATE + ) + @AuthJwtAdminAccessProtected() + @Post('/create') + async create( + @Body() + { username, email, mobileNumber, role, ...body }: UserCreateDto + ): Promise { + const checkRole = await this.roleService.exist(role); + if (!checkRole) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR, + message: 'role.error.notFound', + }); + } + + const usernameExist: boolean = await this.userService.existByUsername( + username + ); + if (usernameExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR, + message: 'user.error.usernameExist', + }); + } + + const emailExist: boolean = await this.userService.existByEmail(email); + if (emailExist) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR, + message: 'user.error.emailExist', + }); + } + + if (mobileNumber) { + const mobileNumberExist: boolean = + await this.userService.existByMobileNumber(mobileNumber); + if (mobileNumberExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR, + message: 'user.error.mobileNumberExist', + }); + } + } + + try { + const password: IAuthPassword = + await this.authService.createPassword(body.password); + + const created: UserDoc = await this.userService.create( + { username, email, mobileNumber, role, ...body }, + password + ); + + return { + data: { _id: created._id }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @UserDeleteDoc() + @Response('user.delete') + @UserDeleteGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_DELETE + ) + @AuthJwtAdminAccessProtected() + @Delete('/delete/:user') + async delete(@GetUser() user: UserDoc): Promise { + try { + await this.userService.delete(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserUpdateDoc() + @Response('user.update', { + serialization: ResponseIdSerialization, + }) + @UserUpdateGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:user') + async update( + @GetUser() user: UserDoc, + @Body() + body: UserUpdateNameDto + ): Promise { + try { + await this.userService.updateName(user, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: user._id }, + }; + } + + @UserInactiveDoc() + @Response('user.inactive') + @UserUpdateInactiveGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/inactive') + async inactive(@GetUser() user: UserDoc): Promise { + try { + await this.userService.inactive(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserActiveDoc() + @Response('user.active') + @UserUpdateActiveGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/active') + async active(@GetUser() user: UserDoc): Promise { + try { + await this.userService.active(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserImportDoc() + @Response('user.import', { + serialization: UserImportSerialization, + }) + @UploadFileSingle('file') + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_CREATE, + ENUM_AUTH_PERMISSIONS.USER_IMPORT + ) + @AuthJwtAdminAccessProtected() + @Post('/import') + async import( + @UploadedFile( + FileRequiredPipe, + FileSizeExcelPipe, + FileTypeExcelPipe, + FileExtractPipe, + new FileValidationPipe(UserImportDto) + ) + file: IFileExtract + ): Promise { + return { data: { file } }; + } + + @UserExportDoc() + @ResponseExcel({ + serialization: UserListSerialization, + fileType: ENUM_HELPER_FILE_TYPE.CSV, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_EXPORT + ) + @AuthJwtAdminAccessProtected() + @HttpCode(HttpStatus.OK) + @Post('/export') + async export(): Promise { + const users: IUserEntity[] = await this.userService.findAll({}); + + return { data: users }; + } + + @UserBlockedDoc() + @Response('user.blocked') + @UserUpdateBlockedGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_BLOCKED + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/blocked') + async blocked(@GetUser() user: UserDoc): Promise { + try { + await this.userService.blocked(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/controllers/user.controller.ts b/src/modules/user/controllers/user.controller.ts new file mode 100644 index 0000000..16df422 --- /dev/null +++ b/src/modules/user/controllers/user.controller.ts @@ -0,0 +1,546 @@ +import { + BadRequestException, + Body, + Controller, + ForbiddenException, + Get, + HttpCode, + HttpStatus, + InternalServerErrorException, + NotFoundException, + Patch, + Post, + UploadedFile, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, + AuthJwtRefreshProtected, + AuthJwtToken, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { UploadFileSingle } 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 { FileSizeImagePipe } from 'src/common/file/pipes/file.size.pipe'; +import { FileTypeImagePipe } from 'src/common/file/pipes/file.type.pipe'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { Logger } from 'src/common/logger/decorators/logger.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { PermissionDoc, PermissionEntity } from "src/modules/permission/repository/entities/permission.entity"; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +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 { GetUser } from 'src/modules/user/decorators/user.decorator'; +import { UserProfileGuard } from 'src/modules/user/decorators/user.public.decorator'; +import { + UserChangePasswordDoc, + UserGrantPermissionDoc, + UserInfoDoc, + UserLoginDoc, + UserProfileDoc, + UserRefreshDoc, + UserUploadProfileDoc, +} from 'src/modules/user/docs/user.doc'; +import { UserChangePasswordDto } from 'src/modules/user/dtos/user.change-password.dto'; +import { UserGrantPermissionDto } from 'src/modules/user/dtos/user.grant-permission.dto'; +import { UserLoginDto } from 'src/modules/user/dtos/user.login.dto'; +import { + IUserDoc, + IUserEntity, +} from 'src/modules/user/interfaces/user.interface'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserGrantPermissionSerialization } from 'src/modules/user/serializations/user.grant-permission.serialization'; +import { UserInfoSerialization } from 'src/modules/user/serializations/user.info.serialization'; +import { UserLoginSerialization } from 'src/modules/user/serializations/user.login.serialization'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { UserProfileSerialization } from 'src/modules/user/serializations/user.profile.serialization'; +import { UserService } from 'src/modules/user/services/user.service'; +import { IAuthPassword } from "../../../common/auth/interfaces/auth.interface"; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +@ApiTags('modules.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserController { + constructor( + private readonly userService: UserService, + private readonly roleService: RoleService, + private readonly awsService: AwsS3Service, + private readonly authService: AuthService, + private readonly settingService: SettingService, + private readonly permissionService: PermissionService + ) {} + + @UserLoginDoc() + @Response('user.login', { + serialization: UserLoginSerialization, + }) + @Logger(ENUM_LOGGER_ACTION.LOGIN, { tags: ['login', 'withEmail'] }) + @HttpCode(HttpStatus.OK) + @Post('/login') + async login( + @Body() { username, password, rememberMe }: UserLoginDto + ): Promise { + const user: UserDoc = await this.userService.findOneByUsername( + username + ); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const passwordAttempt: boolean = + await this.settingService.getPasswordAttempt(); + const maxPasswordAttempt: number = + await this.settingService.getMaxPasswordAttempt(); + if (passwordAttempt && user.passwordAttempt >= maxPasswordAttempt) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR, + message: 'user.error.passwordAttemptMax', + }); + } + + const validate: boolean = await this.authService.validateUser( + password, + user.password + ); + if (!validate) { + try { + await this.userService.increasePasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR, + message: 'user.error.passwordNotMatch', + }); + } else if (user.blocked) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } else if (!user.isActive || user.inactivePermanent) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR, + message: 'role.error.inactive', + }); + } + + try { + await this.userService.resetPasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + const payload: UserPayloadSerialization = + await this.userService.payloadSerialization(userWithRole); + const tokenType: string = await this.authService.getTokenType(); + const expiresIn: number = + await this.authService.getAccessTokenExpirationTime(); + rememberMe = rememberMe ? true : false; + const payloadAccessToken: Record = + await this.authService.createPayloadAccessToken( + payload, + rememberMe + ); + const payloadRefreshToken: Record = + await this.authService.createPayloadRefreshToken( + payload._id, + rememberMe, + { + loginDate: payloadAccessToken.loginDate, + } + ); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedAccessToken: Record | string = + payloadAccessToken; + let payloadHashedRefreshToken: Record | string = + payloadRefreshToken; + + if (payloadEncryption) { + payloadHashedAccessToken = + await this.authService.encryptAccessToken(payloadAccessToken); + payloadHashedRefreshToken = + await this.authService.encryptRefreshToken(payloadRefreshToken); + } + + const accessToken: string = await this.authService.createAccessToken( + payloadHashedAccessToken + ); + + const refreshToken: string = await this.authService.createRefreshToken( + payloadHashedRefreshToken, + { rememberMe } + ); + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + + if (checkPasswordExpired) { + return { + _metadata: { + // override status code and message + customProperty: { + // override status code and message + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR, + message: 'user.error.passwordExpired', + }, + }, + data: { tokenType, expiresIn, accessToken, refreshToken }, + }; + } + + return { + data: { + tokenType, + expiresIn, + accessToken, + refreshToken, + }, + }; + } + + @UserRefreshDoc() + @Response('user.refresh', { serialization: UserLoginSerialization }) + @AuthJwtRefreshProtected() + @HttpCode(HttpStatus.OK) + @Post('/refresh') + async refresh( + @AuthJwtPayload() + { _id, rememberMe, loginDate }: Record, + @AuthJwtToken() refreshToken: string + ): Promise { + const user: UserDoc = await this.userService.findOneById(_id); + + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } else if (user.blocked) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } else if (!user.isActive || user.inactivePermanent) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR, + message: 'role.error.inactive', + }); + } + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + + if (checkPasswordExpired) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR, + message: 'user.error.passwordExpired', + }); + } + + const payload: UserPayloadSerialization = + await this.userService.payloadSerialization(userWithRole); + const tokenType: string = await this.authService.getTokenType(); + const expiresIn: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: Record = + await this.authService.createPayloadAccessToken( + payload, + rememberMe, + { + loginDate, + } + ); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedAccessToken: Record | string = + payloadAccessToken; + + if (payloadEncryption) { + payloadHashedAccessToken = + await this.authService.encryptAccessToken(payloadAccessToken); + } + + const accessToken: string = await this.authService.createAccessToken( + payloadHashedAccessToken + ); + + return { + data: { + tokenType, + expiresIn, + accessToken, + refreshToken, + }, + }; + } + + @UserChangePasswordDoc() + @Response('user.changePassword') + @AuthJwtAccessProtected() + @Patch('/change-password') + async changePassword( + @Body() body: UserChangePasswordDto, + @AuthJwtPayload('_id') _id: string + ): Promise { + const user: UserDoc = await this.userService.findOneById(_id); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const passwordAttempt: boolean = + await this.settingService.getPasswordAttempt(); + const maxPasswordAttempt: number = + await this.settingService.getMaxPasswordAttempt(); + if (passwordAttempt && user.passwordAttempt >= maxPasswordAttempt) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR, + message: 'user.error.passwordAttemptMax', + }); + } + + const matchPassword: boolean = await this.authService.validateUser( + body.oldPassword, + user.password + ); + if (!matchPassword) { + try { + await this.userService.increasePasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR, + message: 'user.error.passwordNotMatch', + }); + } + + const newMatchPassword: boolean = await this.authService.validateUser( + body.newPassword, + user.password + ); + if (newMatchPassword) { + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR, + message: 'user.error.newPasswordMustDifference', + }); + } + + try { + await this.userService.resetPasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + try { + const password: IAuthPassword = + await this.authService.createPassword(body.newPassword); + + await this.userService.updatePassword(user, password); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserInfoDoc() + @Response('user.info', { serialization: UserInfoSerialization }) + @AuthJwtAccessProtected() + @Get('/info') + async info( + @AuthJwtPayload() user: UserPayloadSerialization + ): Promise { + return { data: user }; + } + + @UserGrantPermissionDoc() + @Response('user.grantPermission', { + serialization: UserGrantPermissionSerialization, + }) + @AuthJwtAccessProtected() + @HttpCode(HttpStatus.OK) + @Post('/grant-permission') + async grantPermission( + @AuthJwtPayload() payload: UserPayloadSerialization, + @Body() { scope }: UserGrantPermissionDto + ): Promise { + const user: UserDoc = await this.userService.findOneById(payload._id); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + const permissions: PermissionDoc[] = + await this.permissionService.findAllByIds( + userWithRole.role.permissions + ); + const grantPermissions: IPermissionGroup[] = + await this.permissionService.groupingByGroups(permissions, scope); + + + const payloadPermission: UserPayloadPermissionSerialization = + await this.userService.payloadPermissionSerialization( + user._id, + grantPermissions + ); + + const expiresIn: number = + await this.authService.getPermissionTokenExpirationTime(); + const payloadPermissionToken: Record = + await this.authService.createPayloadPermissionToken( + payloadPermission + ); + + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedPermissionToken: Record | string = + payloadPermissionToken; + + if (payloadEncryption) { + payloadHashedPermissionToken = + await this.authService.encryptPermissionToken( + payloadPermissionToken + ); + } + + const permissionToken: string = + await this.authService.createPermissionToken( + payloadHashedPermissionToken + ); + + return { + data: { permissionToken, expiresIn }, + }; + } + + @UserProfileDoc() + @Response('user.profile', { + serialization: UserProfileSerialization, + }) + @UserProfileGuard() + @AuthJwtAccessProtected() + @Get('/profile') + async profile(@GetUser() user: UserDoc): Promise { + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + return { data: userWithRole.toObject() }; + } + + @UserUploadProfileDoc() + @Response('user.upload') + @UserProfileGuard() + @AuthJwtAccessProtected() + @UploadFileSingle('file') + @HttpCode(HttpStatus.OK) + @Post('/profile/upload') + async upload( + @GetUser() usr: IUserDoc, + @UploadedFile(FileRequiredPipe, FileSizeImagePipe, FileTypeImagePipe) + file: IFile + ): Promise { + const user: UserDoc = await this.userService.findOneById(usr._id); + + const filename: string = file.originalname; + const content: Buffer = file.buffer; + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + + const path = await this.userService.createPhotoFilename(); + + try { + const aws: AwsS3Serialization = + await this.awsService.putItemInBucket( + `${path.filename}.${mime}`, + content, + { + path: `${path.path}/${user._id}`, + } + ); + await this.userService.updatePhoto(user, aws); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/controllers/user.public.controller.ts b/src/modules/user/controllers/user.public.controller.ts new file mode 100644 index 0000000..b327687 --- /dev/null +++ b/src/modules/user/controllers/user.public.controller.ts @@ -0,0 +1,119 @@ +import { + Body, + ConflictException, + Controller, + Delete, + InternalServerErrorException, + Post, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + AuthJwtPayload, + AuthJwtPublicAccessProtected, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +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 { + UserDeleteSelfDoc, + UserSignUpDoc, +} from 'src/modules/user/docs/user.public.doc'; +import { UserSignUpDto } from 'src/modules/user/dtos/user.sign-up.dto'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@ApiTags('modules.public.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserPublicController { + constructor( + private readonly userService: UserService, + private readonly authService: AuthService, + private readonly roleService: RoleService + ) {} + + @UserSignUpDoc() + @Response('user.signUp') + @Post('/sign-up') + async signUp( + @Body() + { email, mobileNumber, username, ...body }: UserSignUpDto + ): Promise { + const role: RoleDoc = await this.roleService.findOneByName('user'); + + const usernameExist: boolean = await this.userService.existByUsername( + username + ); + if (usernameExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR, + message: 'user.error.usernameExist', + }); + } + + const emailExist: boolean = await this.userService.existByEmail(email); + if (emailExist) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR, + message: 'user.error.emailExist', + }); + } + + if (mobileNumber) { + const mobileNumberExist: boolean = + await this.userService.existByMobileNumber(mobileNumber); + if (mobileNumberExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR, + message: 'user.error.mobileNumberExist', + }); + } + } + + try { + const password = await this.authService.createPassword( + body.password + ); + + await this.userService.create( + { email, mobileNumber, username, ...body, role: role._id }, + password + ); + + return; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @UserDeleteSelfDoc() + @Response('user.deleteSelf') + @AuthJwtPublicAccessProtected() + @Delete('/delete') + async deleteSelf(@AuthJwtPayload('_id') _id: string): Promise { + try { + const user: UserDoc = await this.userService.findOneById(_id); + + await this.userService.inactive(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/decorators/user.admin.decorator.ts b/src/modules/user/decorators/user.admin.decorator.ts new file mode 100644 index 0000000..7aa8731 --- /dev/null +++ b/src/modules/user/decorators/user.admin.decorator.ts @@ -0,0 +1,42 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { + USER_ACTIVE_META_KEY, + USER_BLOCKED_META_KEY, +} from 'src/modules/user/constants/user.constant'; +import { UserActiveGuard } from 'src/modules/user/guards/user.active.guard'; +import { UserBlockedGuard } from 'src/modules/user/guards/user.blocked.guard'; +import { UserNotFoundGuard } from 'src/modules/user/guards/user.not-found.guard'; +import { UserPutToRequestGuard } from 'src/modules/user/guards/user.put-to-request.guard'; + +export function UserGetGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserDeleteGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserUpdateGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserActiveGuard), + SetMetadata(USER_ACTIVE_META_KEY, [true]) + ); +} + +export function UserUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserActiveGuard), + SetMetadata(USER_ACTIVE_META_KEY, [false]) + ); +} + +export function UserUpdateBlockedGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserBlockedGuard), + SetMetadata(USER_BLOCKED_META_KEY, [false]) + ); +} diff --git a/src/modules/user/decorators/user.decorator.ts b/src/modules/user/decorators/user.decorator.ts new file mode 100644 index 0000000..7ae9e99 --- /dev/null +++ b/src/modules/user/decorators/user.decorator.ts @@ -0,0 +1,12 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +export const GetUser = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): UserDoc | UserEntity => { + const { __user } = ctx.switchToHttp().getRequest(); + return returnPlain ? __user.toObject() : __user; + } +); diff --git a/src/modules/user/decorators/user.public.decorator.ts b/src/modules/user/decorators/user.public.decorator.ts new file mode 100644 index 0000000..b8608de --- /dev/null +++ b/src/modules/user/decorators/user.public.decorator.ts @@ -0,0 +1,9 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { UserPayloadPutToRequestGuard } from 'src/modules/user/guards/payload/user.payload.put-to-request.guard'; +import { UserNotFoundGuard } from 'src/modules/user/guards/user.not-found.guard'; + +export function UserProfileGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPayloadPutToRequestGuard, UserNotFoundGuard) + ); +} diff --git a/src/modules/user/docs/user.admin.doc.ts b/src/modules/user/docs/user.admin.doc.ts new file mode 100644 index 0000000..b1f0d7b --- /dev/null +++ b/src/modules/user/docs/user.admin.doc.ts @@ -0,0 +1,158 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + UserDocParamsGet, + UserDocQueryBlocked, + UserDocQueryIsActive, +} from 'src/modules/user/constants/user.doc.constant'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; +import { UserImportSerialization } from 'src/modules/user/serializations/user.import.serialization'; +import { UserListSerialization } from 'src/modules/user/serializations/user.list.serialization'; + +export function UserListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('user.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [...UserDocQueryIsActive, ...UserDocQueryBlocked], + }, + response: { + serialization: UserListSerialization, + }, + }) + ); +} + +export function UserGetDoc(): MethodDecorator { + return applyDecorators( + Doc('user.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + response: { serialization: UserGetSerialization }, + }) + ); +} + +export function UserCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('user.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ResponseIdSerialization, + }, + }) + ); +} + +export function UserUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('user.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function UserDeleteDoc(): MethodDecorator { + return applyDecorators( + Doc('user.delete', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserImportDoc(): MethodDecorator { + return applyDecorators( + Doc('user.import', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: UserImportSerialization, + }, + }) + ); +} + +export function UserExportDoc(): MethodDecorator { + return applyDecorators( + Doc('user.export', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.OK, + }, + }) + ); +} + +export function UserActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('user.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('user.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserBlockedDoc(): MethodDecorator { + return applyDecorators( + Doc('user.blocked', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} diff --git a/src/modules/user/docs/user.doc.ts b/src/modules/user/docs/user.doc.ts new file mode 100644 index 0000000..c10576d --- /dev/null +++ b/src/modules/user/docs/user.doc.ts @@ -0,0 +1,98 @@ +import { applyDecorators } from '@nestjs/common'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { UserGrantPermissionSerialization } from 'src/modules/user/serializations/user.grant-permission.serialization'; +import { UserLoginSerialization } from 'src/modules/user/serializations/user.login.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { UserProfileSerialization } from 'src/modules/user/serializations/user.profile.serialization'; + +export function UserProfileDoc(): MethodDecorator { + return applyDecorators( + Doc('user.profile', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserProfileSerialization, + }, + }) + ); +} + +export function UserUploadProfileDoc(): MethodDecorator { + return applyDecorators( + Doc('user.upload', { + auth: { + jwtAccessToken: true, + }, + request: { + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.FORM_DATA, + file: { + multiple: false, + }, + }, + }) + ); +} + +export function UserLoginDoc(): MethodDecorator { + return applyDecorators( + Doc('user.login', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserLoginSerialization, + }, + }) + ); +} + +export function UserRefreshDoc(): MethodDecorator { + return applyDecorators( + Doc('user.refresh', { + auth: { + jwtRefreshToken: true, + }, + response: { + serialization: UserLoginSerialization, + }, + }) + ); +} + +export function UserInfoDoc(): MethodDecorator { + return applyDecorators( + Doc('user.info', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserPayloadSerialization, + }, + }) + ); +} + +export function UserChangePasswordDoc(): MethodDecorator { + return applyDecorators( + Doc('user.changePassword', { + auth: { + jwtAccessToken: true, + }, + }) + ); +} + +export function UserGrantPermissionDoc(): MethodDecorator { + return applyDecorators( + Doc('user.grantPermission', { + response: { + serialization: UserGrantPermissionSerialization, + }, + auth: { + jwtAccessToken: true, + }, + }) + ); +} diff --git a/src/modules/user/docs/user.public.doc.ts b/src/modules/user/docs/user.public.doc.ts new file mode 100644 index 0000000..64b0adb --- /dev/null +++ b/src/modules/user/docs/user.public.doc.ts @@ -0,0 +1,25 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; + +export function UserSignUpDoc(): MethodDecorator { + return applyDecorators( + Doc('user.signUp', { + auth: { + jwtAccessToken: false, + }, + response: { + httpStatus: HttpStatus.CREATED, + }, + }) + ); +} + +export function UserDeleteSelfDoc(): MethodDecorator { + return applyDecorators( + Doc('user.deleteSelf', { + auth: { + jwtAccessToken: true, + }, + }) + ); +} diff --git a/src/modules/user/dtos/user.active.dto.ts b/src/modules/user/dtos/user.active.dto.ts new file mode 100644 index 0000000..26db248 --- /dev/null +++ b/src/modules/user/dtos/user.active.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsDate, IsNotEmpty } from 'class-validator'; + +export class UserActiveDto { + @ApiProperty({ + name: 'isActive', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; + + @ApiProperty({ + name: 'inactiveDate', + description: 'inactive date of user', + required: true, + nullable: false, + }) + @IsDate() + @IsNotEmpty() + @Type(() => Date) + inactiveDate: Date; +} diff --git a/src/modules/user/dtos/user.block.dto.ts b/src/modules/user/dtos/user.block.dto.ts new file mode 100644 index 0000000..24c2703 --- /dev/null +++ b/src/modules/user/dtos/user.block.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsDate, IsNotEmpty } from 'class-validator'; + +export class UserBlockedDto { + @ApiProperty({ + name: 'blocked', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + blocked: boolean; + + @ApiProperty({ + name: 'blockedDate', + required: true, + nullable: false, + }) + @IsDate() + @IsNotEmpty() + @Type(() => Date) + blockedDate: Date; +} diff --git a/src/modules/user/dtos/user.change-password.dto.ts b/src/modules/user/dtos/user.change-password.dto.ts new file mode 100644 index 0000000..069ebbc --- /dev/null +++ b/src/modules/user/dtos/user.change-password.dto.ts @@ -0,0 +1,29 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; +import { IsPasswordStrong } from 'src/common/request/validations/request.is-password-strong.validation'; + +export class UserChangePasswordDto { + @ApiProperty({ + description: + "new string password, newPassword can't same with oldPassword", + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsPasswordStrong() + @IsNotEmpty() + readonly newPassword: string; + + @ApiProperty({ + description: 'old string password', + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsString() + @IsNotEmpty() + readonly oldPassword: string; +} diff --git a/src/modules/user/dtos/user.create.dto.ts b/src/modules/user/dtos/user.create.dto.ts new file mode 100644 index 0000000..7a927b7 --- /dev/null +++ b/src/modules/user/dtos/user.create.dto.ts @@ -0,0 +1,91 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + IsEmail, + MaxLength, + MinLength, + IsUUID, + IsOptional, + ValidateIf, +} from 'class-validator'; +import { IsPasswordStrong } from 'src/common/request/validations/request.is-password-strong.validation'; +import { MobileNumberAllowed } from 'src/common/request/validations/request.mobile-number-allowed.validation'; + +export class UserCreateDto { + @ApiProperty({ + example: faker.internet.userName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + @Type(() => String) + readonly username: string; + + @ApiProperty({ + example: faker.internet.email(), + required: true, + }) + @IsEmail() + @IsNotEmpty() + @MaxLength(100) + @Type(() => String) + readonly email: string; + + @ApiProperty({ + example: faker.name.firstName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(30) + @Type(() => String) + readonly firstName: string; + + @ApiProperty({ + example: faker.name.lastName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(30) + @Type(() => String) + readonly lastName: string; + + @ApiProperty({ + example: faker.phone.number('62812#########'), + required: true, + }) + @IsString() + @IsOptional() + @MinLength(10) + @MaxLength(14) + @ValidateIf((e) => e.mobileNumber !== '') + @Type(() => String) + @MobileNumberAllowed() + readonly mobileNumber?: string; + + @ApiProperty({ + example: faker.datatype.uuid(), + required: true, + }) + @IsNotEmpty() + @IsUUID('4') + readonly role: string; + + @ApiProperty({ + description: 'string password', + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsNotEmpty() + @IsPasswordStrong() + readonly password: string; +} diff --git a/src/modules/user/dtos/user.grant-permission.dto.ts b/src/modules/user/dtos/user.grant-permission.dto.ts new file mode 100644 index 0000000..563fb0a --- /dev/null +++ b/src/modules/user/dtos/user.grant-permission.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsEnum, IsArray, ArrayNotEmpty } from 'class-validator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class UserGrantPermissionDto { + @ApiProperty({ + description: 'scope for grant permission', + example: Object.values(ENUM_PERMISSION_GROUP), + required: true, + isArray: true, + }) + @IsEnum(ENUM_PERMISSION_GROUP, { each: true }) + @IsNotEmpty() + @IsArray() + @ArrayNotEmpty() + readonly scope: ENUM_PERMISSION_GROUP[]; +} diff --git a/src/modules/user/dtos/user.import.dto.ts b/src/modules/user/dtos/user.import.dto.ts new file mode 100644 index 0000000..5fe2ef6 --- /dev/null +++ b/src/modules/user/dtos/user.import.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserImportDto extends OmitType(UserCreateDto, [ + 'role', + 'password', +] as const) {} diff --git a/src/modules/user/dtos/user.login.dto.ts b/src/modules/user/dtos/user.login.dto.ts new file mode 100644 index 0000000..6f590e8 --- /dev/null +++ b/src/modules/user/dtos/user.login.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, ValidateIf } from 'class-validator'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; + +export class UserLoginDto extends PickType(UserCreateDto, [ + 'username', + 'password', +] as const) { + @ApiProperty({ + description: + 'if true refresh token expired will extend to 30d, else 7d', + example: false, + required: false, + }) + @IsOptional() + @IsBoolean() + @ValidateIf((e) => e.rememberMe !== '') + readonly rememberMe?: boolean; +} diff --git a/src/modules/user/dtos/user.password-attempt.dto.ts b/src/modules/user/dtos/user.password-attempt.dto.ts new file mode 100644 index 0000000..e578d1c --- /dev/null +++ b/src/modules/user/dtos/user.password-attempt.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class UserPasswordAttemptDto { + @ApiProperty({ + name: 'passwordAttempt', + description: 'password attempt of user', + required: true, + nullable: false, + }) + @IsNumber() + @IsNotEmpty() + passwordAttempt: number; +} diff --git a/src/modules/user/dtos/user.password-expired.dto.ts b/src/modules/user/dtos/user.password-expired.dto.ts new file mode 100644 index 0000000..239aa5e --- /dev/null +++ b/src/modules/user/dtos/user.password-expired.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { UserPasswordDto } from 'src/modules/user/dtos/user.password.dto'; + +export class UserPasswordExpiredDto extends PickType(UserPasswordDto, [ + 'passwordExpired', +] as const) {} diff --git a/src/modules/user/dtos/user.password.dto.ts b/src/modules/user/dtos/user.password.dto.ts new file mode 100644 index 0000000..d61ed65 --- /dev/null +++ b/src/modules/user/dtos/user.password.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDate, IsNotEmpty, IsString } from 'class-validator'; + +export class UserPasswordDto { + @ApiProperty({ + name: 'password', + description: 'password hash of string password', + required: true, + nullable: false, + }) + @IsString() + @IsNotEmpty() + password: string; + + @ApiProperty({ + name: 'passwordExpired', + description: 'password expired date', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsDate() + @Type(() => Date) + passwordExpired: Date; + + @ApiProperty({ + name: 'passwordCreated', + description: 'password created date', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsDate() + @Type(() => Date) + passwordCreated: Date; + + @ApiProperty({ + name: 'salt', + required: true, + nullable: false, + }) + @IsString() + @IsNotEmpty() + salt: string; +} diff --git a/src/modules/user/dtos/user.photo.dto.ts b/src/modules/user/dtos/user.photo.dto.ts new file mode 100644 index 0000000..f7755ff --- /dev/null +++ b/src/modules/user/dtos/user.photo.dto.ts @@ -0,0 +1,9 @@ +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; + +export class UserPhotoDto { + @ValidateNested() + @Type(() => AwsS3Serialization) + photo: AwsS3Serialization; +} diff --git a/src/modules/user/dtos/user.request.dto.ts b/src/modules/user/dtos/user.request.dto.ts new file mode 100644 index 0000000..83095ae --- /dev/null +++ b/src/modules/user/dtos/user.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class UserRequestDto { + @ApiProperty({ + name: 'user', + description: 'user id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + user: string; +} diff --git a/src/modules/user/dtos/user.sign-up.dto.ts b/src/modules/user/dtos/user.sign-up.dto.ts new file mode 100644 index 0000000..8d05a7f --- /dev/null +++ b/src/modules/user/dtos/user.sign-up.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserSignUpDto extends OmitType(UserCreateDto, ['role'] as const) {} diff --git a/src/modules/user/dtos/user.update-name.dto.ts b/src/modules/user/dtos/user.update-name.dto.ts new file mode 100644 index 0000000..0c6e8c6 --- /dev/null +++ b/src/modules/user/dtos/user.update-name.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserUpdateNameDto extends PickType(UserCreateDto, [ + 'firstName', + 'lastName', +] as const) {} diff --git a/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts b/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts new file mode 100644 index 0000000..db024b2 --- /dev/null +++ b/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class UserPayloadPutToRequestGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { user } = request; + + const check: UserDoc = await this.userService.findOneById(user._id, { + join: true, + }); + request.__user = check; + + return true; + } +} diff --git a/src/modules/user/guards/user.active.guard.ts b/src/modules/user/guards/user.active.guard.ts new file mode 100644 index 0000000..21d5a09 --- /dev/null +++ b/src/modules/user/guards/user.active.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { USER_ACTIVE_META_KEY } from 'src/modules/user/constants/user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + USER_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __user } = context.switchToHttp().getRequest(); + + if (!required.includes(__user.isActive)) { + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR, + message: 'user.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/user/guards/user.blocked.guard.ts b/src/modules/user/guards/user.blocked.guard.ts new file mode 100644 index 0000000..9115d31 --- /dev/null +++ b/src/modules/user/guards/user.blocked.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { USER_BLOCKED_META_KEY } from 'src/modules/user/constants/user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserBlockedGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + USER_BLOCKED_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __user } = context.switchToHttp().getRequest(); + + if (!required.includes(__user.blocked)) { + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } + return true; + } +} diff --git a/src/modules/user/guards/user.not-found.guard.ts b/src/modules/user/guards/user.not-found.guard.ts new file mode 100644 index 0000000..e8bc208 --- /dev/null +++ b/src/modules/user/guards/user.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __user } = context.switchToHttp().getRequest(); + + if (!__user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + return true; + } +} diff --git a/src/modules/user/guards/user.put-to-request.guard.ts b/src/modules/user/guards/user.put-to-request.guard.ts new file mode 100644 index 0000000..98c998d --- /dev/null +++ b/src/modules/user/guards/user.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class UserPutToRequestGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { user } = params; + + const check: UserDoc = await this.userService.findOneById(user, { + join: true, + }); + request.__user = check; + + return true; + } +} diff --git a/src/modules/user/interfaces/user.interface.ts b/src/modules/user/interfaces/user.interface.ts new file mode 100644 index 0000000..3a86765 --- /dev/null +++ b/src/modules/user/interfaces/user.interface.ts @@ -0,0 +1,16 @@ +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +export interface IUserEntity extends Omit { + role: RoleEntity; +} + +export interface IUserDoc extends Omit { + role: RoleDoc; +} diff --git a/src/modules/user/interfaces/user.service.interface.ts b/src/modules/user/interfaces/user.service.interface.ts new file mode 100644 index 0000000..14a3217 --- /dev/null +++ b/src/modules/user/interfaces/user.service.interface.ts @@ -0,0 +1,125 @@ +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +export interface IUserService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById(_id: string, options?: IDatabaseFindOneOptions): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByUsername( + username: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + { + username, + firstName, + lastName, + email, + mobileNumber, + role, + }: UserCreateDto, + { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, + options?: IDatabaseCreateOptions + ): Promise; + + existByEmail( + email: string, + options?: IDatabaseExistOptions + ): Promise; + + joinWithRole(repository: UserDoc): Promise; + + existByMobileNumber( + mobileNumber: string, + options?: IDatabaseExistOptions + ): Promise; + + existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise; + + delete(repository: UserDoc): Promise; + + updateName( + repository: UserDoc, + { firstName, lastName }: UserUpdateNameDto + ): Promise; + + updatePhoto( + repository: UserDoc, + photo: AwsS3Serialization + ): Promise; + + updatePassword( + repository: UserDoc, + { passwordHash, passwordExpired, salt, passwordCreated }: IAuthPassword + ): Promise; + + active(repository: UserDoc): Promise; + + inactive(repository: UserDoc): Promise; + + blocked(repository: UserDoc): Promise; + + unblocked(repository: UserDoc): Promise; + + maxPasswordAttempt(repository: UserDoc): Promise; + + increasePasswordAttempt(repository: UserDoc): Promise; + + resetPasswordAttempt(repository: UserDoc): Promise; + + updatePasswordExpired( + repository: UserDoc, + passwordExpired: Date + ): Promise; + + createPhotoFilename(): Promise>; + + payloadSerialization(data: IUserDoc): Promise; + + payloadPermissionSerialization( + _id: string, + permissions: IPermissionGroup[] + ): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/modules/user/repository/entities/user.entity.ts b/src/modules/user/repository/entities/user.entity.ts new file mode 100644 index 0000000..e81d1a8 --- /dev/null +++ b/src/modules/user/repository/entities/user.entity.ts @@ -0,0 +1,168 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; + +export const UserDatabaseName = 'users'; + +@DatabaseEntity({ collection: UserDatabaseName }) +export class UserEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + trim: true, + unique: true, + type: String, + maxlength: 100, + }) + username: string; + + @Prop({ + required: true, + index: true, + lowercase: true, + trim: true, + type: String, + maxlength: 50, + }) + firstName: string; + + @Prop({ + required: true, + index: true, + lowercase: true, + trim: true, + type: String, + maxlength: 50, + }) + lastName: string; + + @Prop({ + required: false, + sparse: true, + trim: true, + unique: true, + type: String, + maxlength: 15, + }) + mobileNumber?: string; + + @Prop({ + required: true, + index: true, + unique: true, + trim: true, + lowercase: true, + type: String, + maxlength: 100, + }) + email: string; + + @Prop({ + required: true, + ref: RoleEntity.name, + index: true, + }) + role: string; + + @Prop({ + required: true, + type: String, + }) + password: string; + + @Prop({ + required: true, + type: Date, + }) + passwordExpired: Date; + + @Prop({ + required: true, + type: Date, + }) + passwordCreated: Date; + + @Prop({ + required: true, + default: 0, + type: Number, + }) + passwordAttempt: number; + + @Prop({ + required: true, + type: Date, + }) + signUpDate: Date; + + @Prop({ + required: true, + type: String, + }) + salt: string; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: true, + default: false, + index: true, + type: Boolean, + }) + inactivePermanent: boolean; + + @Prop({ + required: false, + type: Date, + }) + inactiveDate?: Date; + + @Prop({ + required: true, + default: false, + index: true, + type: Boolean, + }) + blocked: boolean; + + @Prop({ + required: false, + type: Date, + }) + blockedDate?: Date; + + @Prop({ + required: false, + _id: false, + type: { + path: String, + pathWithFilename: String, + filename: String, + completedUrl: String, + baseUrl: String, + mime: String, + }, + }) + photo?: AwsS3Serialization; +} + +export const UserSchema = SchemaFactory.createForClass(UserEntity); + +export type UserDoc = UserEntity & Document; + +UserSchema.pre('save', function (next: CallbackWithoutResultAndOptionalError) { + this.email = this.email.toLowerCase(); + this.firstName = this.firstName.toLowerCase(); + this.lastName = this.lastName.toLowerCase(); + + next(); +}); diff --git a/src/modules/user/repository/repositories/user.repository.ts b/src/modules/user/repository/repositories/user.repository.ts new file mode 100644 index 0000000..a28fd3d --- /dev/null +++ b/src/modules/user/repository/repositories/user.repository.ts @@ -0,0 +1,27 @@ +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 { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class UserRepository extends DatabaseMongoUUIDRepositoryAbstract< + UserEntity, + UserDoc +> { + constructor( + @DatabaseModel(UserEntity.name) + private readonly userModel: Model + ) { + super(userModel, { + path: 'role', + localField: 'role', + foreignField: '_id', + model: RoleEntity.name, + }); + } +} diff --git a/src/modules/user/repository/user.repository.module.ts b/src/modules/user/repository/user.repository.module.ts new file mode 100644 index 0000000..739abcc --- /dev/null +++ b/src/modules/user/repository/user.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + UserEntity, + UserSchema, +} from 'src/modules/user/repository/entities/user.entity'; +import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; + +@Module({ + providers: [UserRepository], + exports: [UserRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: UserEntity.name, + schema: UserSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class UserRepositoryModule {} diff --git a/src/modules/user/serializations/user.get.serialization.ts b/src/modules/user/serializations/user.get.serialization.ts new file mode 100644 index 0000000..60b8b3d --- /dev/null +++ b/src/modules/user/serializations/user.get.serialization.ts @@ -0,0 +1,119 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; + +export class UserGetSerialization { + @ApiProperty({ example: faker.datatype.uuid() }) + @Type(() => String) + readonly _id: string; + + @ApiProperty({ + type: () => RoleGetSerialization, + }) + @Type(() => RoleGetSerialization) + readonly role: RoleGetSerialization; + + @ApiProperty({ + example: faker.internet.userName(), + }) + readonly username: string; + + @ApiProperty({ + example: faker.internet.email(), + }) + readonly email: string; + + @ApiProperty({ + example: faker.internet.email(), + }) + readonly mobileNumber?: string; + + @ApiProperty({ + example: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + example: true, + }) + readonly inactivePermanent: boolean; + + @ApiProperty({ + required: false, + nullable: true, + example: faker.date.recent(), + }) + readonly inactiveDate?: Date; + + @ApiProperty({ + example: false, + }) + readonly blocked: boolean; + + @ApiProperty({ + required: false, + nullable: true, + example: faker.date.recent(), + }) + readonly blockedDate?: Date; + + @ApiProperty({ + example: faker.name.firstName(), + }) + readonly firstName: string; + + @ApiProperty({ + example: faker.name.lastName(), + }) + readonly lastName: string; + + @ApiProperty({ + allOf: [{ $ref: getSchemaPath(AwsS3Serialization) }], + }) + readonly photo?: AwsS3Serialization; + + @Exclude() + readonly password: string; + + @ApiProperty({ + example: faker.date.future(), + }) + readonly passwordExpired: Date; + + @ApiProperty({ + example: faker.date.past(), + }) + readonly passwordCreated: Date; + + @ApiProperty({ + example: [1, 0], + }) + readonly passwordAttempt: number; + + @ApiProperty({ + example: faker.date.recent(), + }) + readonly signUpDate: Date; + + @Exclude() + readonly salt: string; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/user/serializations/user.grant-permission.serialization.ts b/src/modules/user/serializations/user.grant-permission.serialization.ts new file mode 100644 index 0000000..2fc98ba --- /dev/null +++ b/src/modules/user/serializations/user.grant-permission.serialization.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UserGrantPermissionSerialization { + @ApiProperty({ + example: 'Bearer', + required: true, + }) + readonly tokenType: string; + + @ApiProperty({ + example: 1660190937231, + description: 'Expire in timestamp', + required: true, + }) + readonly expiresIn: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + readonly permissionToken: string; +} diff --git a/src/modules/user/serializations/user.import.serialization.ts b/src/modules/user/serializations/user.import.serialization.ts new file mode 100644 index 0000000..f6fbbaf --- /dev/null +++ b/src/modules/user/serializations/user.import.serialization.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserImportSerialization { + @ApiProperty({ + description: 'Data extract from excel', + example: [{}, {}], + type: 'array', + }) + extract: Record[]; + + @ApiProperty({ + description: 'Data after validation with dto', + example: [{}, {}], + type: 'array', + }) + dto: Record[]; +} diff --git a/src/modules/user/serializations/user.info.serialization.ts b/src/modules/user/serializations/user.info.serialization.ts new file mode 100644 index 0000000..5f95eb2 --- /dev/null +++ b/src/modules/user/serializations/user.info.serialization.ts @@ -0,0 +1,13 @@ +import { PickType } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; + +export class UserInfoSerialization extends PickType(UserPayloadSerialization, [ + '_id', + 'username', + 'rememberMe', + 'loginDate', +] as const) { + readonly role: string; + readonly accessFor: ENUM_AUTH_ACCESS_FOR; +} diff --git a/src/modules/user/serializations/user.list.serialization.ts b/src/modules/user/serializations/user.list.serialization.ts new file mode 100644 index 0000000..262f818 --- /dev/null +++ b/src/modules/user/serializations/user.list.serialization.ts @@ -0,0 +1,39 @@ +import { OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserGetSerialization } from './user.get.serialization'; + +export class UserListSerialization extends OmitType(UserGetSerialization, [ + 'role', + 'photo', + 'passwordExpired', + 'passwordCreated', + 'passwordAttempt', + 'signUpDate', + 'inactiveDate', + 'blockedDate', +] as const) { + @Exclude() + readonly role: string; + + @Exclude() + readonly photo?: AwsS3Serialization; + + @Exclude() + readonly passwordExpired: Date; + + @Exclude() + readonly passwordCreated: Date; + + @Exclude() + readonly passwordAttempt: number; + + @Exclude() + readonly signUpDate: Date; + + @Exclude() + readonly inactiveDate?: Date; + + @Exclude() + readonly blockedDate?: Date; +} diff --git a/src/modules/user/serializations/user.login.serialization.ts b/src/modules/user/serializations/user.login.serialization.ts new file mode 100644 index 0000000..4d39882 --- /dev/null +++ b/src/modules/user/serializations/user.login.serialization.ts @@ -0,0 +1,32 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UserLoginSerialization { + @ApiProperty({ + example: 'Bearer', + required: true, + }) + readonly tokenType: string; + + @ApiProperty({ + example: 1660190937231, + description: 'Expire in timestamp', + required: true, + }) + readonly expiresIn: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + readonly accessToken: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + @ApiProperty() + readonly refreshToken: string; +} diff --git a/src/modules/user/serializations/user.payload-permission.serialization.ts b/src/modules/user/serializations/user.payload-permission.serialization.ts new file mode 100644 index 0000000..72b12bd --- /dev/null +++ b/src/modules/user/serializations/user.payload-permission.serialization.ts @@ -0,0 +1,20 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; + +export class UserPayloadPermissionSerialization extends PickType( + UserGetSerialization, + ['_id'] as const +) { + @ApiProperty({ + example: [faker.name.jobTitle(), faker.name.jobTitle()], + type: 'string', + isArray: true, + }) + @Transform( + ({ value }) => value?.map((val: PermissionEntity) => val.code) ?? [] + ) + readonly permissions: string[]; +} diff --git a/src/modules/user/serializations/user.payload.serialization.ts b/src/modules/user/serializations/user.payload.serialization.ts new file mode 100644 index 0000000..c8c89bb --- /dev/null +++ b/src/modules/user/serializations/user.payload.serialization.ts @@ -0,0 +1,81 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Exclude, Expose, Transform } from 'class-transformer'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; + +export class UserPayloadSerialization extends OmitType(UserGetSerialization, [ + 'photo', + 'role', + 'isActive', + 'blocked', + 'email', + 'mobileNumber', + 'passwordExpired', + 'passwordCreated', + 'passwordAttempt', + 'signUpDate', + 'inactiveDate', + 'blockedDate', + 'createdAt', + 'updatedAt', +] as const) { + @Exclude() + readonly photo?: AwsS3Serialization; + + @ApiProperty({ + example: faker.datatype.uuid(), + type: 'string', + }) + @Transform(({ obj }) => `${obj.role._id}`) + readonly role: string; + + @ApiProperty({ + example: ENUM_AUTH_ACCESS_FOR.ADMIN, + type: 'string', + enum: ENUM_AUTH_ACCESS_FOR, + }) + @Expose() + @Transform(({ obj }) => obj.role.accessFor) + readonly accessFor: ENUM_AUTH_ACCESS_FOR; + + @Exclude() + readonly isActive: boolean; + + @Exclude() + readonly blocked: boolean; + + @Exclude() + readonly passwordExpired: Date; + + @Exclude() + readonly passwordCreated: Date; + + @Exclude() + readonly passwordAttempt: number; + + @Exclude() + readonly signUpDate: Date; + + @Exclude() + readonly inactiveDate?: Date; + + @Exclude() + readonly blockedDate?: Date; + + @Exclude() + readonly email: Date; + + @Exclude() + readonly mobileNumber?: number; + + readonly rememberMe: boolean; + readonly loginDate: Date; + + @Exclude() + readonly createdAt: number; + + @Exclude() + readonly updatedAt: number; +} diff --git a/src/modules/user/serializations/user.profile.serialization.ts b/src/modules/user/serializations/user.profile.serialization.ts new file mode 100644 index 0000000..7709da3 --- /dev/null +++ b/src/modules/user/serializations/user.profile.serialization.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { UserGetSerialization } from './user.get.serialization'; + +export class UserProfileSerialization extends OmitType(UserGetSerialization, [ + 'passwordAttempt', +] as const) { + @Exclude() + passwordAttempt: number; +} diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..57457b7 --- /dev/null +++ b/src/modules/user/services/user.service.ts @@ -0,0 +1,284 @@ +import { Injectable } from '@nestjs/common'; +import { IUserService } from 'src/modules/user/interfaces/user.service.interface'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +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 { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { plainToInstance } from 'class-transformer'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { RoleEntity } from "../../role/repository/entities/role.entity"; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +@Injectable() +export class UserService implements IUserService { + private readonly uploadPath: string; + + constructor( + private readonly userRepository: UserRepository, + private readonly helperDateService: HelperDateService, + private readonly helperStringService: HelperStringService, + private readonly configService: ConfigService + ) { + this.uploadPath = this.configService.get('user.uploadPath'); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findAll(find, { + ...options, + join: true, + }); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOne(find, options); + } + + async findOneByUsername( + username: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOne({ username }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.userRepository.getTotal(find, options); + } + + async create( + { + username, + firstName, + lastName, + email, + mobileNumber, + role, + }: UserCreateDto, + { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, + options?: IDatabaseCreateOptions + ): Promise { + const create: UserEntity = new UserEntity(); + create.username = username; + create.firstName = firstName; + create.email = email; + create.password = passwordHash; + create.role = role; + create.isActive = true; + create.inactivePermanent = false; + create.blocked = false; + create.lastName = lastName; + create.salt = salt; + create.passwordExpired = passwordExpired; + create.passwordCreated = passwordCreated; + create.signUpDate = this.helperDateService.create(); + create.passwordAttempt = 0; + create.mobileNumber = mobileNumber ?? undefined; + + return this.userRepository.create(create, options); + } + + async existByEmail( + email: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { + email: { + $regex: new RegExp(`\\b${email}\\b`), + $options: 'i', + }, + }, + { ...options, withDeleted: true } + ); + } + + async existByMobileNumber( + mobileNumber: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { + mobileNumber, + }, + { ...options, withDeleted: true } + ); + } + + async existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { username }, + { ...options, withDeleted: true } + ); + } + + async delete(repository: UserDoc): Promise { + return this.userRepository.softDelete(repository); + } + + async updateName( + repository: UserDoc, + { firstName, lastName }: UserUpdateNameDto + ): Promise { + repository.firstName = firstName; + repository.lastName = lastName; + + return this.userRepository.save(repository); + } + + async updatePhoto( + repository: UserDoc, + photo: AwsS3Serialization + ): Promise { + repository.photo = photo; + + return this.userRepository.save(repository); + } + + async updatePassword( + repository: UserDoc, + { passwordHash, passwordExpired, salt, passwordCreated }: IAuthPassword + ): Promise { + repository.password = passwordHash; + repository.passwordExpired = passwordExpired; + repository.passwordCreated = passwordCreated; + repository.salt = salt; + + return this.userRepository.save(repository); + } + + async active(repository: UserDoc): Promise { + repository.isActive = true; + repository.inactiveDate = undefined; + + return this.userRepository.save(repository); + } + + async inactive(repository: UserDoc): Promise { + repository.isActive = false; + repository.inactiveDate = this.helperDateService.create(); + + return this.userRepository.save(repository); + } + + async blocked(repository: UserDoc): Promise { + repository.blocked = true; + repository.blockedDate = this.helperDateService.create(); + + return this.userRepository.save(repository); + } + + async unblocked(repository: UserDoc): Promise { + repository.blocked = false; + repository.blockedDate = undefined; + + return this.userRepository.save(repository); + } + + async maxPasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = 3; + + return this.userRepository.save(repository); + } + + async increasePasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = ++repository.passwordAttempt; + + return this.userRepository.save(repository); + } + + async resetPasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = 0; + + return this.userRepository.save(repository); + } + + async updatePasswordExpired( + repository: UserDoc, + passwordExpired: Date + ): Promise { + repository.passwordExpired = passwordExpired; + + return this.userRepository.save(repository); + } + + async joinWithRole(repository: UserDoc): Promise { + return repository.populate({ + path: 'role', + localField: 'role', + foreignField: '_id', + model: RoleEntity.name, + }); + } + + async createPhotoFilename(): Promise> { + const filename: string = this.helperStringService.random(20); + + return { + path: this.uploadPath, + filename: filename, + }; + } + + async payloadSerialization( + data: IUserDoc + ): Promise { + return plainToInstance(UserPayloadSerialization, data.toObject()); + } + + async payloadPermissionSerialization( + _id: string, + permissions: IPermissionGroup[] + ): Promise { + const permissionEntity: PermissionEntity[] = permissions + .map((val) => val.permissions) + .flat(1); + return plainToInstance(UserPayloadPermissionSerialization, { + _id, + permissions: permissionEntity, + }); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.userRepository.deleteMany(find, options); + } +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..88e46e9 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UserRepositoryModule } from 'src/modules/user/repository/user.repository.module'; +import { UserService } from './services/user.service'; +@Module({ + imports: [UserRepositoryModule], + exports: [UserService], + providers: [UserService], + controllers: [], +}) +export class UserModule {} diff --git a/src/router/router.module.ts b/src/router/router.module.ts new file mode 100644 index 0000000..8847459 --- /dev/null +++ b/src/router/router.module.ts @@ -0,0 +1,59 @@ +import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; +import { RouterModule as NestJsRouterModule } from '@nestjs/core'; +import { RoutesAdminModule } from './routes/routes.admin.module'; +import { RoutesCallbackModule } from './routes/routes.callback.module'; +import { RoutesModule } from './routes/routes.module'; +import { RoutesPublicModule } from './routes/routes.public.module'; +import { RoutesTestModule } from './routes/routes.test.module'; + +@Module({}) +export class RouterModule { + static forRoot(): DynamicModule { + const imports: ( + | DynamicModule + | Type + | Promise + | ForwardReference + )[] = []; + + if (process.env.HTTP_ENABLE === 'true') { + imports.push( + RoutesModule, + RoutesTestModule, + RoutesPublicModule, + RoutesAdminModule, + RoutesCallbackModule, + NestJsRouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + { + path: '/test', + module: RoutesTestModule, + }, + { + path: '/public', + module: RoutesPublicModule, + }, + { + path: '/admin', + module: RoutesAdminModule, + }, + { + path: '/callback', + module: RoutesCallbackModule, + }, + ]) + ); + } + + return { + module: RouterModule, + providers: [], + exports: [], + controllers: [], + imports, + }; + } +} diff --git a/src/router/routes/routes.admin.module.ts b/src/router/routes/routes.admin.module.ts new file mode 100644 index 0000000..2a03d1c --- /dev/null +++ b/src/router/routes/routes.admin.module.ts @@ -0,0 +1,31 @@ +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 { SettingAdminController } from 'src/common/setting/controllers/setting.admin.controller'; +import { PermissionAdminController } from 'src/modules/permission/controllers/permission.admin.controller'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { RoleAdminController } from 'src/modules/role/controllers/role.admin.controller'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserAdminController } from 'src/modules/user/controllers/user.admin.controller'; +import { UserModule } from 'src/modules/user/user.module'; + +@Module({ + controllers: [ + SettingAdminController, + ApiKeyAdminController, + PermissionAdminController, + RoleAdminController, + UserAdminController, + ], + providers: [], + exports: [], + imports: [ + AuthModule, + ApiKeyModule, + PermissionModule, + RoleModule, + UserModule, + ], +}) +export class RoutesAdminModule {} diff --git a/src/router/routes/routes.callback.module.ts b/src/router/routes/routes.callback.module.ts new file mode 100644 index 0000000..a6dd3ac --- /dev/null +++ b/src/router/routes/routes.callback.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +@Module({ + controllers: [], + providers: [], + exports: [], + imports: [], +}) +export class RoutesCallbackModule {} diff --git a/src/router/routes/routes.module.ts b/src/router/routes/routes.module.ts new file mode 100644 index 0000000..55952b0 --- /dev/null +++ b/src/router/routes/routes.module.ts @@ -0,0 +1,37 @@ +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { AwsModule } from 'src/common/aws/aws.module'; +import { MessageController } from 'src/common/message/controllers/message.controller'; +import { SettingController } from 'src/common/setting/controllers/setting.controller'; +import { HealthController } from 'src/health/controllers/health.controller'; +import { HealthModule } from 'src/health/health.module'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserController } from 'src/modules/user/controllers/user.controller'; +import { UserModule } from 'src/modules/user/user.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { ApiKeyController } from 'src/common/api-key/controllers/api-key.controller'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; + +@Module({ + controllers: [ + HealthController, + SettingController, + MessageController, + UserController, + ApiKeyController, + ], + providers: [], + exports: [], + imports: [ + AwsModule, + TerminusModule, + AuthModule, + HealthModule, + RoleModule, + UserModule, + PermissionModule, + ApiKeyModule, + ], +}) +export class RoutesModule {} diff --git a/src/router/routes/routes.public.module.ts b/src/router/routes/routes.public.module.ts new file mode 100644 index 0000000..81ac8ab --- /dev/null +++ b/src/router/routes/routes.public.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { UserPublicController } from "../../modules/user/controllers/user.public.controller"; +import { UserModule } from "../../modules/user/user.module"; +import { AuthModule } from "../../common/auth/auth.module"; +import { RoleModule } from "../../modules/role/role.module"; +// import { AuthModule } from 'src/common/auth/auth.module'; +// import { RoleModule } from 'src/modules/role/role.module'; +// import { UserPublicController } from 'src/modules/user/controllers/user.public.controller'; +// import { UserModule } from 'src/modules/user/user.module'; + +// @Module({ +// controllers: [UserPublicController], +// providers: [], +// exports: [], +// imports: [UserModule, AuthModule, RoleModule], +// }) +// export class RoutesPublicModule {} + +@Module({ + controllers: [UserPublicController], + providers: [], + exports: [], + imports: [UserModule, AuthModule, RoleModule], +}) +export class RoutesPublicModule {} diff --git a/src/router/routes/routes.test.module.ts b/src/router/routes/routes.test.module.ts new file mode 100644 index 0000000..dc2e46e --- /dev/null +++ b/src/router/routes/routes.test.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +@Module({ + controllers: [], + providers: [], + exports: [], + imports: [], +}) +export class RoutesTestModule {} diff --git a/src/swagger.ts b/src/swagger.ts new file mode 100644 index 0000000..b56145a --- /dev/null +++ b/src/swagger.ts @@ -0,0 +1,82 @@ +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 { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { ResponseDefaultSerialization } from 'src/common/response/serializations/response.default.serialization'; +import { ResponsePagingSerialization } from 'src/common/response/serializations/response.paging.serialization'; + +export default async function (app: NestApplication) { + const configService = app.get(ConfigService); + const env: string = configService.get('app.env'); + const logger = new Logger(); + + const docName: string = configService.get('doc.name'); + const docDesc: string = configService.get('doc.description'); + const docVersion: string = configService.get('doc.version'); + const docPrefix: string = configService.get('doc.prefix'); + + if (env !== ENUM_APP_ENVIRONMENT.PRODUCTION) { + const documentBuild = new DocumentBuilder() + .setTitle(docName) + .setDescription(docDesc) + .setVersion(docVersion) + .addTag("API's") + .addServer(`/`) + .addServer(`/staging`) + .addServer(`/production`) + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'accessToken' + ) + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'refreshToken' + ) + .addApiKey( + { type: 'apiKey', in: 'header', name: 'x-api-key' }, + 'apiKey' + ) + .addApiKey( + { + type: 'apiKey', + in: 'header', + name: 'x-permission-token', + description: 'grant permission for /admin prefix endpoints', + }, + 'permissionToken' + ) + .build(); + + const document = SwaggerModule.createDocument(app, documentBuild, { + deepScanRoutes: true, + extraModels: [ + ResponseDefaultSerialization, + ResponsePagingSerialization, + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, + AwsS3Serialization, + ], + }); + + SwaggerModule.setup(docPrefix, app, document, { + explorer: true, + customSiteTitle: docName, + }); + + logger.log( + `==========================================================` + ); + + logger.log(`Docs will serve on ${docPrefix}`, 'NestApplication'); + + logger.log( + `==========================================================` + ); + } +} diff --git a/test/e2e/api-key/api-key.admin.e2e-spec.ts b/test/e2e/api-key/api-key.admin.e2e-spec.ts new file mode 100644 index 0000000..df33b90 --- /dev/null +++ b/test/e2e/api-key/api-key.admin.e2e-spec.ts @@ -0,0 +1,282 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { + E2E_API_KEY_ADMIN_ACTIVE_URL, + E2E_API_KEY_ADMIN_INACTIVE_URL, + E2E_API_KEY_ADMIN_UPDATE_DATE_URL, +} from 'test/e2e/api-key/api-key.constant'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { faker } from '@faker-js/faker'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; + +describe('E2E Api Key Admin', () => { + let app: INestApplication; + let authService: AuthService; + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + + let accessToken: string; + let permissionToken: string; + + let apiKey: ApiKeyDoc; + let apiKeyExpired: ApiKeyDoc; + const apiKeyCreate = { + name: `${faker.name.firstName()}${faker.random.alphaNumeric(20)}`, + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + apiKeyService = app.get(ApiKeyService); + helperDateService = app.get(HelperDateService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + const apiKeyCreated1 = await apiKeyService.create( + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + }); + + apiKey = await apiKeyService.findOneById(apiKeyCreated1.doc._id); + + const apiKeyCreated2 = await apiKeyService.create( + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + startDate: helperDateService.backwardInDays(7), + endDate: helperDateService.backwardInDays(1), + }); + + apiKeyExpired = await apiKeyService.findOneById(apiKeyCreated2.doc._id); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKey._id, + }); + + await apiKeyService.deleteMany({ + name: apiKeyCreate.name, + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Active not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Expired`, async () => { + await apiKeyService.inactive(apiKeyExpired); + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKeyExpired._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + await apiKeyService.active(apiKeyExpired); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} already Active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_INACTIVE_URL.replace( + ':_id', + apiKeyExpired._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_INACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_INACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace( + ':_id', + apiKeyExpired._id + ) + ) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace(':_id', apiKey._id)) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/api-key/api-key.constant.ts b/test/e2e/api-key/api-key.constant.ts new file mode 100644 index 0000000..2ab6c52 --- /dev/null +++ b/test/e2e/api-key/api-key.constant.ts @@ -0,0 +1,12 @@ +export const E2E_API_KEY_ADMIN_UPDATE_DATE_URL = + '/admin/api-key/update/:_id/date'; +export const E2E_API_KEY_ADMIN_UPDATE_RESET_URL = '/api-key/update/:_id/reset'; +export const E2E_API_KEY_ADMIN_ACTIVE_URL = '/admin/api-key/update/:_id/active'; +export const E2E_API_KEY_ADMIN_INACTIVE_URL = + '/admin/api-key/update/:_id/inactive'; + + +export const E2E_API_KEY_ADMIN_LIST_URL = '/api-key/list'; +export const E2E_API_KEY_ADMIN_GET_URL = '/api-key/get/:_id'; +export const E2E_API_KEY_ADMIN_CREATE_URL = '/api-key/create'; +export const E2E_API_KEY_ADMIN_UPDATE_NAME_URL = '/api-key/update/:_id'; \ No newline at end of file diff --git a/test/e2e/api-key/api-key.e2e-spec.ts b/test/e2e/api-key/api-key.e2e-spec.ts new file mode 100644 index 0000000..c0f755b --- /dev/null +++ b/test/e2e/api-key/api-key.e2e-spec.ts @@ -0,0 +1,276 @@ +import { HttpStatus, INestApplication } from "@nestjs/common"; +import { AuthService } from "../../../src/common/auth/services/auth.service"; +import { ApiKeyService } from "../../../src/common/api-key/services/api-key.service"; +import { HelperDateService } from "../../../src/common/helper/services/helper.date.service"; +import { ApiKeyDoc } from "../../../src/common/api-key/repository/entities/api-key.entity"; +import { faker } from '@faker-js/faker'; +import * as process from "process"; +import { Test } from "@nestjs/testing"; +import { CommonModule } from "../../../src/common/common.module"; +import { RoutesModule } from "../../../src/router/routes/routes.module"; +import { RouterModule } from "@nestjs/core"; +import { useContainer } from "class-validator"; +import { E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST } from "../user/user.constant"; +import { + E2E_API_KEY_ADMIN_CREATE_URL, + E2E_API_KEY_ADMIN_GET_URL, + E2E_API_KEY_ADMIN_LIST_URL, E2E_API_KEY_ADMIN_UPDATE_NAME_URL, E2E_API_KEY_ADMIN_UPDATE_RESET_URL +} from "./api-key.constant"; +import request from "supertest"; +import { DatabaseDefaultUUID } from "../../../src/common/database/constants/database.function.constant"; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from "../../../src/common/api-key/constants/api-key.status-code.constant"; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from "../../../src/common/request/constants/request.status-code.constant"; +import { response } from "express"; + +describe('E2E Api Key', () => { + let app: INestApplication; + let authService: AuthService; + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + + let accessToken: string; + let permissionToken: string; + + let apiKey: ApiKeyDoc; + let apiKeyExpired: ApiKeyDoc; + + const apiKeyCreate = { + name: `${faker.name.firstName()}${faker.random.alphaNumeric(20)}`, + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), {fallbackOnErrors: true}); + authService = app.get(AuthService); + apiKeyService = app.get(ApiKeyService); + helperDateService = app.get(HelperDateService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date() + }, + false + ); + + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id + }); + + const apiKeyCreated1 = await apiKeyService.create( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + } + ); + + apiKey = await apiKeyService.findOneById(apiKeyCreated1.doc._id); + + const apiKeyCreated2 = await apiKeyService.create( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + startDate: helperDateService.backwardInDays(7), + endDate: helperDateService.backwardInDays(1), + } + ); + + apiKeyExpired = await apiKeyService.findOneById(apiKeyCreated2.doc._id); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKey._id, + }); + + await apiKeyService.deleteMany({ + name: apiKeyCreate.name + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_API_KEY_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_API_KEY_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_API_KEY_ADMIN_GET_URL} GET Not Found`, async () => { + console.log(apiKey._id); + const response = await request(app.getHttpServer()) + .get( + E2E_API_KEY_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_API_KEY_ADMIN_GET_URL} GET Success`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_API_KEY_ADMIN_GET_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_API_KEY_ADMIN_CREATE_URL} Create Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_API_KEY_ADMIN_CREATE_URL) + .send({ + name: [1231231] + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + ); + }); + + it(`POST ${E2E_API_KEY_ADMIN_CREATE_URL} Create Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_API_KEY_ADMIN_CREATE_URL) + .send(apiKeyCreate) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Reset Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace( + ':_id', + DatabaseDefaultUUID() + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace( + ':_id', + apiKeyExpired._id + )) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it( `PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Reset Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace(':_id', apiKey._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Error request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace(':_id', apiKey._id)) + .send({ + name: [], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send({ + name: faker.name.jobArea(), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace( + ':_id', + apiKey._id + )) + .send({ + name: faker.name.jobArea() + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + +}) \ No newline at end of file diff --git a/test/e2e/jest.json b/test/e2e/jest.json new file mode 100644 index 0000000..e658ae6 --- /dev/null +++ b/test/e2e/jest.json @@ -0,0 +1,30 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/e2e/api-key/*.e2e-spec.ts", + "/test/e2e/permission/*.e2e-spec.ts", + "/test/e2e/role/*.e2e-spec.ts", + "/test/e2e/setting/*.e2e-spec.ts", + "/test/e2e/user/*.e2e-spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage-e2e", + "collectCoverageFrom": [ + "./src/common/api-key/controllers/**", + "./src/common/setting/controllers/**", + "./src/modules/**/controllers/**" + ], + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/e2e/permission/permission.admin.e2e-spec.ts b/test/e2e/permission/permission.admin.e2e-spec.ts new file mode 100644 index 0000000..8310660 --- /dev/null +++ b/test/e2e/permission/permission.admin.e2e-spec.ts @@ -0,0 +1,285 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { + E2E_PERMISSION_ADMIN_ACTIVE_URL, + E2E_PERMISSION_ADMIN_GET_URL, + E2E_PERMISSION_ADMIN_GROUP_URL, + E2E_PERMISSION_ADMIN_INACTIVE_URL, + E2E_PERMISSION_ADMIN_LIST_URL, + E2E_PERMISSION_ADMIN_UPDATE_URL, +} from './permission.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; + +describe('E2E Permission Admin', () => { + let app: INestApplication; + let authService: AuthService; + let permissionService: PermissionService; + + let accessToken: string; + let permissionToken: string; + let permission: PermissionEntity; + + const updateData: PermissionUpdateDescriptionDto = { + description: 'UPDATE_ROLE', + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + permissionService = app.get(PermissionService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + permission = await permissionService.create({ + code: 'TEST_PERMISSION_XXXX', + description: 'test description', + group: ENUM_PERMISSION_GROUP.PERMISSION, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await permissionService.deleteMany({ + code: 'TEST_PERMISSION_XXXX', + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GROUP_URL} Group Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_GROUP_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_PERMISSION_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_GET_URL.replace(':_id', permission._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace(':_id', permission._id) + ) + .send({ + name: [1231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace(':_id', permission._id) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} Active not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} already Active`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace(':_id', permission._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + permission._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + permission._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} Active Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace(':_id', permission._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/permission/permission.constant.ts b/test/e2e/permission/permission.constant.ts new file mode 100644 index 0000000..d2b713c --- /dev/null +++ b/test/e2e/permission/permission.constant.ts @@ -0,0 +1,8 @@ +export const E2E_PERMISSION_ADMIN_LIST_URL = '/admin/permission/list'; +export const E2E_PERMISSION_ADMIN_GROUP_URL = '/admin/permission/group'; +export const E2E_PERMISSION_ADMIN_GET_URL = '/admin/permission/get/:_id'; +export const E2E_PERMISSION_ADMIN_UPDATE_URL = '/admin/permission/update/:_id'; +export const E2E_PERMISSION_ADMIN_ACTIVE_URL = + '/admin/permission/update/:_id/active'; +export const E2E_PERMISSION_ADMIN_INACTIVE_URL = + '/admin/permission/update/:_id/inactive'; diff --git a/test/e2e/role/role.admin.e2e-spec.ts b/test/e2e/role/role.admin.e2e-spec.ts new file mode 100644 index 0000000..659cea6 --- /dev/null +++ b/test/e2e/role/role.admin.e2e-spec.ts @@ -0,0 +1,498 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { + E2E_ROLE_ACCESS_FOR_URL, + E2E_ROLE_ADMIN_ACTIVE_URL, + E2E_ROLE_ADMIN_CREATE_URL, + E2E_ROLE_ADMIN_DELETE_URL, + E2E_ROLE_ADMIN_GET_BY_ID_URL, + E2E_ROLE_ADMIN_INACTIVE_URL, + E2E_ROLE_ADMIN_LIST_URL, + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL, + E2E_ROLE_ADMIN_UPDATE_URL, +} from './role.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; + +describe('E2E Role Admin', () => { + let app: INestApplication; + let authService: AuthService; + let roleService: RoleService; + let permissionService: PermissionService; + + let role: RoleEntity; + let roleUpdate: RoleEntity; + + let accessToken: string; + let permissionToken: string; + + let successData: RoleCreateDto; + let updateData: RoleUpdateNameDto; + let updateDataPermission: RoleUpdatePermissionDto; + let existData: RoleCreateDto; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + roleService = app.get(RoleService); + permissionService = app.get(PermissionService); + + const permissions: PermissionEntity[] = await permissionService.findAll( + { + code: { + $in: [ + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_CREATE, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_DELETE, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ], + }, + } + ); + + successData = { + name: 'testRole1', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + roleUpdate = await roleService.create({ + name: 'testRole2', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }); + + updateData = { + name: 'testRole3', + }; + + updateDataPermission = { + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + existData = { + name: 'testRole', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + role = await roleService.create(existData); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await roleService.deleteMany({ + name: 'testRole', + }); + await roleService.deleteMany({ + name: 'testRole1', + }); + await roleService.deleteMany({ + name: 'testRole2', + }); + await roleService.deleteMany({ + name: 'testRole3', + }); + } catch (e) {} + + await app.close(); + }); + + it(`GET ${E2E_ROLE_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_ROLE_ADMIN_GET_BY_ID_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_ROLE_ADMIN_GET_BY_ID_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_ROLE_ADMIN_GET_BY_ID_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ADMIN_GET_BY_ID_URL.replace(':_id', role._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send({ + name: 123123, + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send(existData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Permission Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send({ + ...successData, + permissions: [DatabaseDefaultUUID()], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send(successData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send({ + name: [231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Exist`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send(existData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send({ + name: [231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateDataPermission) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Permission Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send({ + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + permissions: [DatabaseDefaultUUID()], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send(updateDataPermission) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_ROLE_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_INACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_INACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_ROLE_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_ACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, already active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_ACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR + ); + }); + + it(`DELETE ${E2E_ROLE_ADMIN_DELETE_URL} Delete Not Found`, async () => { + const response = await request(app.getHttpServer()) + .delete( + E2E_ROLE_ADMIN_DELETE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`DELETE ${E2E_ROLE_ADMIN_DELETE_URL} Delete Success`, async () => { + const response = await request(app.getHttpServer()) + .delete(E2E_ROLE_ADMIN_DELETE_URL.replace(':_id', role._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_ROLE_ACCESS_FOR_URL} Access For Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ACCESS_FOR_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/role/role.constant.ts b/test/e2e/role/role.constant.ts new file mode 100644 index 0000000..869cf37 --- /dev/null +++ b/test/e2e/role/role.constant.ts @@ -0,0 +1,10 @@ +export const E2E_ROLE_ADMIN_LIST_URL = '/admin/role/list'; +export const E2E_ROLE_ADMIN_GET_BY_ID_URL = '/admin/role/get/:_id'; +export const E2E_ROLE_ADMIN_CREATE_URL = '/admin/role/create'; +export const E2E_ROLE_ADMIN_UPDATE_URL = '/admin/role/update/:_id'; +export const E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL = + '/admin/role/update/:_id/permission'; +export const E2E_ROLE_ADMIN_DELETE_URL = '/admin/role/delete/:_id'; +export const E2E_ROLE_ADMIN_INACTIVE_URL = '/admin/role/update/:_id/inactive'; +export const E2E_ROLE_ADMIN_ACTIVE_URL = '/admin/role/update/:_id/active'; +export const E2E_ROLE_ACCESS_FOR_URL = '/admin/role/access-for'; diff --git a/test/e2e/setting/setting.admin.e2e-spec.ts b/test/e2e/setting/setting.admin.e2e-spec.ts new file mode 100644 index 0000000..6cd9b47 --- /dev/null +++ b/test/e2e/setting/setting.admin.e2e-spec.ts @@ -0,0 +1,181 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { useContainer } from 'class-validator'; +import { E2E_SETTING_ADMIN_UPDATE_URL } from './setting.constant'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; + +describe('E2E Setting Admin', () => { + let app: INestApplication; + let settingService: SettingService; + let authService: AuthService; + + let setting: SettingEntity; + const settingName: string = faker.random.alphaNumeric(10); + + let accessToken: string; + let permissionToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + settingService = app.get(SettingService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await settingService.create({ + name: settingName, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + setting = await settingService.findOneByName(settingName); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ name: settingName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_SETTING_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'true', type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + value: { test: 'aaa', type: ENUM_SETTING_DATA_TYPE.STRING }, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Value Not Allowed`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + value: 'test', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_VALUE_NOT_ALLOWED_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update String Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'test', type: ENUM_SETTING_DATA_TYPE.STRING }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Number Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 123, type: ENUM_SETTING_DATA_TYPE.NUMBER }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update String Convert If Possible Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'false', type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Boolean Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: false, type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/setting/setting.constant.ts b/test/e2e/setting/setting.constant.ts new file mode 100644 index 0000000..f64b415 --- /dev/null +++ b/test/e2e/setting/setting.constant.ts @@ -0,0 +1,6 @@ +export const E2E_SETTING_COMMON_LIST_URL = '/setting/list'; +export const E2E_SETTING_COMMON_GET_URL = '/setting/get/:_id'; +export const E2E_SETTING_COMMON_GET_BY_NAME_URL = + '/setting/get/name/:settingName'; + +export const E2E_SETTING_ADMIN_UPDATE_URL = '/admin/setting/update/:_id'; diff --git a/test/e2e/setting/setting.e2e-spec.ts b/test/e2e/setting/setting.e2e-spec.ts new file mode 100644 index 0000000..2a12980 --- /dev/null +++ b/test/e2e/setting/setting.e2e-spec.ts @@ -0,0 +1,126 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { useContainer } from 'class-validator'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { + E2E_SETTING_COMMON_GET_BY_NAME_URL, + E2E_SETTING_COMMON_GET_URL, + E2E_SETTING_COMMON_LIST_URL, +} from './setting.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E Setting', () => { + let app: INestApplication; + let settingService: SettingService; + + let setting: SettingEntity; + const settingName: string = faker.random.alphaNumeric(10); + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + settingService = app.get(SettingService); + + await settingService.create({ + name: settingName, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + setting = await settingService.findOneByName(settingName); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ name: settingName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_SETTING_COMMON_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_LIST_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_URL.replace(':_id', `${setting._id}`) + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_BY_NAME_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_BY_NAME_URL.replace( + ':settingName', + faker.name.firstName() + ) + ); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_BY_NAME_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_BY_NAME_URL.replace( + ':settingName', + setting.name + ) + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/files/import.csv b/test/e2e/user/files/import.csv new file mode 100644 index 0000000..d391bea --- /dev/null +++ b/test/e2e/user/files/import.csv @@ -0,0 +1,2 @@ +username,email,firstName,lastName,mobileNumber +test111,test111@mail.com,test111,tesssst,6281276542233 diff --git a/test/e2e/user/files/medium.jpg b/test/e2e/user/files/medium.jpg new file mode 100644 index 0000000..60b5d52 Binary files /dev/null and b/test/e2e/user/files/medium.jpg differ diff --git a/test/e2e/user/files/small.jpg b/test/e2e/user/files/small.jpg new file mode 100644 index 0000000..eba421b Binary files /dev/null and b/test/e2e/user/files/small.jpg differ diff --git a/test/e2e/user/files/test.txt b/test/e2e/user/files/test.txt new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/test/e2e/user/files/test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/test/e2e/user/user.admin.e2e-spec.ts b/test/e2e/user/user.admin.e2e-spec.ts new file mode 100644 index 0000000..c41015b --- /dev/null +++ b/test/e2e/user/user.admin.e2e-spec.ts @@ -0,0 +1,476 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { + E2E_USER_ADMIN_ACTIVE_URL, + E2E_USER_ADMIN_BLOCKED_URL, + E2E_USER_ADMIN_CREATE_URL, + E2E_USER_ADMIN_DELETE_URL, + E2E_USER_ADMIN_EXPORT_URL, + E2E_USER_ADMIN_GET_URL, + E2E_USER_ADMIN_IMPORT_URL, + E2E_USER_ADMIN_INACTIVE_URL, + E2E_USER_ADMIN_LIST_URL, + E2E_USER_ADMIN_UPDATE_URL, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from './user.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +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 { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; + +describe('E2E User Admin', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let userData: Record; + let userExist: UserDoc; + + let accessToken: string; + let permissionToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + userData = { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password: password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + username: faker.internet.userName(), + role: `${role._id}`, + }; + + const passwordHash = await authService.createPassword(password); + + userExist = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const user = await userService.findOne( + { + email: 'superadmin@mail.com', + }, + { + join: true, + } + ); + const map = await userService.payloadSerialization(user); + const payload = await authService.createPayloadAccessToken(map, false); + + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ + _id: { $in: [userData._id, userExist._id] }, + }); + await userService.deleteMany({ username: 'test111' }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_USER_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + role: 'test_roles', + accessFor: 'test', + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Role Not Found`, async () => { + const datauser = { + ...userData, + role: `${DatabaseDefaultUUID()}`, + password, + }; + + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send(datauser); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Username Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + username: userExist.username, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Email Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + email: userExist.email, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Phone Number Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + mobileNumber: userExist.mobileNumber, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send(userData); + + userData = response.body.data; + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`GET ${E2E_USER_ADMIN_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_USER_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_USER_ADMIN_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_ADMIN_GET_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_USER_ADMIN_UPDATE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: [], + lastName: 1231231, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_USER_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_USER_ADMIN_UPDATE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_INACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_INACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_ACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, already active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_ACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_BLOCKED_URL} Blocked, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_BLOCKED_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_BLOCKED_URL} Blocked, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_BLOCKED_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`DELETE ${E2E_USER_ADMIN_DELETE_URL} Delete, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .delete( + E2E_USER_ADMIN_DELETE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`DELETE ${E2E_USER_ADMIN_DELETE_URL} Delete, success`, async () => { + const userBlocked = await userService.findOneByUsername( + userExist.username + ); + await userService.blocked(userBlocked); + + const response = await request(app.getHttpServer()) + .delete(E2E_USER_ADMIN_DELETE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_ADMIN_IMPORT_URL} Import Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_IMPORT_URL) + .attach('file', './test/e2e/user/files/import.csv') + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`POST ${E2E_USER_ADMIN_EXPORT_URL} Export Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_EXPORT_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.change-password.e2e-spec.ts b/test/e2e/user/user.change-password.e2e-spec.ts new file mode 100644 index 0000000..6f87973 --- /dev/null +++ b/test/e2e/user/user.change-password.e2e-spec.ts @@ -0,0 +1,196 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_CHANGE_PASSWORD_URL } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E User Change Password', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + const newPassword = `bbBB@!456`; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: '123123', + newPassword: '123', + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword, + }) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Old Password Not Match`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: 'as1231dAA@@!', + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Old Password Password Attempt Max`, async () => { + await userService.maxPasswordAttempt(user); + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: 'as1231dAA@@!', + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR + ); + + await userService.resetPasswordAttempt(user); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} New Password must different with old password`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword: password, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.constant.ts b/test/e2e/user/user.constant.ts new file mode 100644 index 0000000..dd5b46f --- /dev/null +++ b/test/e2e/user/user.constant.ts @@ -0,0 +1,39 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export const E2E_USER_ADMIN_LIST_URL = '/admin/user/list'; +export const E2E_USER_ADMIN_GET_URL = '/admin/user/get/:_id'; +export const E2E_USER_ADMIN_ACTIVE_URL = '/admin/user/update/:_id/active'; +export const E2E_USER_ADMIN_INACTIVE_URL = '/admin/user/update/:_id/inactive'; +export const E2E_USER_ADMIN_CREATE_URL = '/admin/user/create'; +export const E2E_USER_ADMIN_UPDATE_URL = '/admin/user/update/:_id'; +export const E2E_USER_ADMIN_DELETE_URL = '/admin/user/delete/:_id'; +export const E2E_USER_ADMIN_IMPORT_URL = '/admin/user/import'; +export const E2E_USER_ADMIN_EXPORT_URL = '/admin/user/export'; +export const E2E_USER_ADMIN_BLOCKED_URL = '/admin/user/update/:_id/blocked'; + +export const E2E_USER_PROFILE_URL = '/user/profile'; +export const E2E_USER_PROFILE_UPLOAD_URL = '/user/profile/upload'; +export const E2E_USER_LOGIN_URL = '/user/login'; +export const E2E_USER_REFRESH_URL = '/user/refresh'; +export const E2E_USER_CHANGE_PASSWORD_URL = '/user/change-password'; +export const E2E_USER_INFO = '/user/info'; +export const E2E_USER_GRANT_PERMISSION = '/user/grant-permission'; + +export const E2E_USER_PUBLIC_SIGN_UP_URL = '/public/user/sign-up'; +export const E2E_USER_PUBLIC_DELETE_URL = '/public/user/delete'; + +export const E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST = { + role: '613ee8e5b2fdd012b94484cb', + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + phoneNumber: '628123123112', + email: 'test@kadence.com', + _id: '613ee8e5b2fdd012b94484ca', + rememberMe: false, + loginWith: 'EMAIL', + loginDate: '2021-9-13', +}; + +export const E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST = { + permissions: [], + _id: '613ee8e5b2fdd012b94484ca', +}; diff --git a/test/e2e/user/user.e2e-spec.ts b/test/e2e/user/user.e2e-spec.ts new file mode 100644 index 0000000..970128e --- /dev/null +++ b/test/e2e/user/user.e2e-spec.ts @@ -0,0 +1,164 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { + E2E_USER_PROFILE_UPLOAD_URL, + E2E_USER_PROFILE_URL, +} from './user.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E User', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (e) {} + + await app.close(); + }); + + it(`GET ${E2E_USER_PROFILE_URL} Profile Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_PROFILE_URL) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_USER_PROFILE_URL} Profile`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_PROFILE_URL) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .attach('file', './test/e2e/user/files/test.txt') + .set('Authorization', `Bearer ${accessToken}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.UNSUPPORTED_MEDIA_TYPE); + expect(response.body.statusCode).toEqual( + ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR + ); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .attach('file', './test/e2e/user/files/test.txt') + .set('Authorization', `Bearer ${accessTokenNotFound}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .send() + .attach('file', './test/e2e/user/files/small.jpg') + .set('Authorization', `Bearer ${accessToken}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.grant-permission.e2e-spec.ts b/test/e2e/user/user.grant-permission.e2e-spec.ts new file mode 100644 index 0000000..1a68f2f --- /dev/null +++ b/test/e2e/user/user.grant-permission.e2e-spec.ts @@ -0,0 +1,245 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_GRANT_PERMISSION } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +describe('E2E User Grant Password', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: '123123', + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); + +describe('E2E User Grant Password Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + + let user: UserEntity; + + let accessToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadHashedAccessToken = await authService.encryptAccessToken( + payload + ); + + accessToken = await authService.createAccessToken( + payloadHashedAccessToken + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.info.e2e-spec.ts b/test/e2e/user/user.info.e2e-spec.ts new file mode 100644 index 0000000..ffb4790 --- /dev/null +++ b/test/e2e/user/user.info.e2e-spec.ts @@ -0,0 +1,63 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { useContainer } from 'class-validator'; +import { CommonModule } from 'src/common/common.module'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_INFO, +} from 'test/e2e/user/user.constant'; + +describe('E2E User', () => { + let app: INestApplication; + let authService: AuthService; + + let accessToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + + const payload = await authService.createPayloadAccessToken( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + false + ); + accessToken = await authService.createAccessToken(payload); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${E2E_USER_INFO} Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_INFO) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.login.e2e-spec.ts b/test/e2e/user/user.login.e2e-spec.ts new file mode 100644 index 0000000..1ce332f --- /dev/null +++ b/test/e2e/user/user.login.e2e-spec.ts @@ -0,0 +1,344 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { E2E_USER_LOGIN_URL } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_AUTH_ACCESS_FOR_DEFAULT } from 'src/common/auth/constants/auth.enum.constant'; +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 { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +describe('E2E User Login', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + let helperDateService: HelperDateService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserDoc; + let role: RoleDoc; + const roleName = faker.random.alphaNumeric(5); + let passwordExpired: Date; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + helperDateService = app.get(HelperDateService); + + await roleService.create({ + name: roleName, + accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT.USER, + permissions: [], + }); + role = await roleService.findOneByName(roleName); + + passwordExpired = helperDateService.backwardInDays(5); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + await roleService.deleteMany({ name: roleName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: [1231], + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: faker.internet.userName(), + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Not Match`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password: 'Password@@1231', + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Attempt Max`, async () => { + await userService.maxPasswordAttempt(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password: 'Password@@1231', + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR + ); + + await userService.resetPasswordAttempt(user); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Blocked`, async () => { + await userService.blocked(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + + await userService.unblocked(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Inactive`, async () => { + await userService.inactive(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + await userService.active(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Role Inactive`, async () => { + await roleService.inactive(role); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + await roleService.active(role); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Expired`, async () => { + await userService.updatePasswordExpired(user, passwordExpired); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR + ); + }); +}); + +describe('E2E User Login Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserEntity; + const roleName = faker.random.alphaNumeric(5); + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + await roleService.create({ + name: roleName, + accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT.USER, + permissions: [], + }); + const role: RoleDoc = await roleService.findOneByName(roleName); + + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + await app.init(); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: true, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.public.e2e-spec.ts b/test/e2e/user/user.public.e2e-spec.ts new file mode 100644 index 0000000..6c190c4 --- /dev/null +++ b/test/e2e/user/user.public.e2e-spec.ts @@ -0,0 +1,171 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesPublicModule } from 'src/router/routes/routes.public.module'; +import { + E2E_USER_PUBLIC_DELETE_URL, + E2E_USER_PUBLIC_SIGN_UP_URL, +} from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +describe('E2E User Public', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + + const password = `@!aaAA@123`; + let userData: Record; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesPublicModule, + AuthModule, + RouterModule.register([ + { + path: '/public', + module: RoutesPublicModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + + userData = { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password: password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + username: faker.internet.userName(), + }; + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ + email: userData.email, + mobileNumber: userData.mobileNumber, + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + username: faker.name.firstName().toLowerCase(), + email: faker.name.firstName().toLowerCase(), + firstName: faker.name.firstName().toLowerCase(), + lastName: faker.name.lastName().toLowerCase(), + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send(userData); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Username Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + mobileNumber: faker.phone.number('62812#########'), + email: faker.internet.email(), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Email Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + username: faker.internet.userName(), + mobileNumber: faker.phone.number('62812#########'), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Mobile Number Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + username: faker.internet.userName(), + email: faker.internet.email(), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR + ); + }); + + it(`DELETE ${E2E_USER_PUBLIC_DELETE_URL} Success`, async () => { + const user = await userService.findOneByUsername( + userData.username, + { + join: true, + } + ); + const map = await userService.payloadSerialization(user); + const payload = await authService.createPayloadAccessToken(map, false); + const accessToken = await authService.createAccessToken(payload); + + const response = await request(app.getHttpServer()) + .delete(E2E_USER_PUBLIC_DELETE_URL) + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.refresh.e2e-spec.ts b/test/e2e/user/user.refresh.e2e-spec.ts new file mode 100644 index 0000000..f3a32c6 --- /dev/null +++ b/test/e2e/user/user.refresh.e2e-spec.ts @@ -0,0 +1,297 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_REFRESH_URL } from './user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +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 { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; + +describe('E2E User Refresh', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + let helperDateService: HelperDateService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserDoc; + let role: RoleDoc; + let passwordExpired: Date; + let passwordExpiredForward: Date; + + let refreshToken: string; + let refreshTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + helperDateService = app.get(HelperDateService); + + role = await roleService.findOneByName('user'); + + passwordExpired = helperDateService.backwardInDays(5); + passwordExpiredForward = helperDateService.forwardInDays(5); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadRefreshToken( + map._id, + false + ); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + refreshToken = await authService.createRefreshToken(payload, { + rememberMe: false, + notBeforeExpirationTime: '0', + }); + refreshTokenNotFound = await authService.createRefreshToken( + payloadNotFound, + { + rememberMe: false, + notBeforeExpirationTime: '0', + } + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshTokenNotFound}`); + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Blocked`, async () => { + await userService.blocked(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.unblocked(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Inactive`, async () => { + await userService.inactive(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.active(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Role Inactive`, async () => { + await roleService.inactive(role); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await roleService.active(role); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Password Expired`, async () => { + await userService.updatePasswordExpired(user, passwordExpired); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.updatePasswordExpired(user, passwordExpiredForward); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); + +describe('E2E User Refresh Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserEntity; + + let refreshToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadRefreshToken( + map._id, + false + ); + + const payloadHashedRefreshToken: string = + await authService.encryptRefreshToken(payload); + + refreshToken = await authService.createRefreshToken( + payloadHashedRefreshToken, + { + rememberMe: true, + notBeforeExpirationTime: '0', + } + ); + + await app.init(); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/aws/aws.s3.constant.ts b/test/integration/aws/aws.s3.constant.ts new file mode 100644 index 0000000..a1c803a --- /dev/null +++ b/test/integration/aws/aws.s3.constant.ts @@ -0,0 +1 @@ +export const INTEGRATION_AWS_URL = '/health/aws'; diff --git a/test/integration/aws/aws.s3.integration.spec.ts b/test/integration/aws/aws.s3.integration.spec.ts new file mode 100644 index 0000000..d6fba0f --- /dev/null +++ b/test/integration/aws/aws.s3.integration.spec.ts @@ -0,0 +1,36 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { INTEGRATION_AWS_URL } from './aws.s3.constant'; +import request from 'supertest'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; + +describe('Aws S3 Integration', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CommonModule, RoutesModule], + controllers: [], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${INTEGRATION_AWS_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + INTEGRATION_AWS_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/database/database.constant.ts b/test/integration/database/database.constant.ts new file mode 100644 index 0000000..cb35f64 --- /dev/null +++ b/test/integration/database/database.constant.ts @@ -0,0 +1 @@ +export const INTEGRATION_DATABASE_URL = '/health/database'; diff --git a/test/integration/database/database.integration.spec.ts b/test/integration/database/database.integration.spec.ts new file mode 100644 index 0000000..81c1f11 --- /dev/null +++ b/test/integration/database/database.integration.spec.ts @@ -0,0 +1,36 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { INTEGRATION_DATABASE_URL } from './database.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; + +describe('Database Integration', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CommonModule, RoutesModule], + controllers: [], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${INTEGRATION_DATABASE_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + INTEGRATION_DATABASE_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/jest.json b/test/integration/jest.json new file mode 100644 index 0000000..3b00338 --- /dev/null +++ b/test/integration/jest.json @@ -0,0 +1,32 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/integration/**/*.spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage-integration", + "collectCoverageFrom": [ + "./integration" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/unit/api-key/api-key.service.spec.ts b/test/unit/api-key/api-key.service.spec.ts new file mode 100644 index 0000000..c778224 --- /dev/null +++ b/test/unit/api-key/api-key.service.spec.ts @@ -0,0 +1,503 @@ +import { Test } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ConfigModule } from '@nestjs/config'; +import configs from 'src/configs'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; + +describe('ApiKeyService', () => { + const apiKeyName1: string = faker.random.alphaNumeric(15); + const apiKeyName2: string = faker.random.alphaNumeric(15); + const apiKeyName3: string = faker.random.alphaNumeric(15); + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + let helperHashService: HelperHashService; + let apiKeyCreated: IApiKeyCreated; + let apiKey: ApiKeyDoc; + let startDate: Date; + let endDate: Date; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ApiKeyModule, + ], + }).compile(); + + apiKeyService = moduleRef.get(ApiKeyService); + helperDateService = moduleRef.get(HelperDateService); + helperHashService = moduleRef.get(HelperHashService); + + apiKeyCreated = await apiKeyService.create({ + name: apiKeyName1, + description: faker.random.alphaNumeric(20), + }); + + apiKey = await apiKeyService.findOneById(apiKeyCreated.doc._id); + + startDate = helperDateService.backwardInDays(1); + endDate = helperDateService.forwardInDays(20); + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKeyCreated.doc._id, + }); + await apiKeyService.deleteMany({ + name: { $in: [apiKeyName1, apiKeyName2, apiKeyName3] }, + }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(apiKeyService).toBeDefined(); + }); + + describe('findAll', () => { + it('should return the array of apikeys', async () => { + const result: ApiKeyEntity[] = await apiKeyService.findAll( + { name: apiKeyName1 }, + { + paging: { limit: 1, offset: 0 }, + order: { name: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC }, + } + ); + + jest.spyOn(apiKeyService, 'findAll').mockReturnValueOnce( + result as any + ); + + const newApiKey: ApiKeyEntity = { + name: apiKeyCreated.doc.name, + description: apiKeyCreated.doc.description, + key: apiKeyCreated.doc.key, + hash: apiKeyCreated.doc.hash, + isActive: apiKeyCreated.doc.isActive, + _id: apiKeyCreated.doc._id, + createdAt: apiKeyCreated.doc.createdAt, + updatedAt: apiKeyCreated.doc.updatedAt, + }; + + expect(result).toBeTruthy(); + expect(result.length).toBe(1); + expect(result[0]).toEqual(newApiKey); + expect(result[0]._id).toBe(apiKeyCreated.doc._id); + expect(result[0].key).toBe(apiKeyCreated.doc.key); + }); + }); + + describe('findOneById', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneById( + apiKeyCreated.doc._id + ); + + jest.spyOn(apiKeyService, 'findOneById').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneById( + faker.datatype.uuid() + ); + + jest.spyOn(apiKeyService, 'findOneById').mockReturnValueOnce(null); + + expect(result).toBeNull(); + }); + }); + + describe('findOne', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOne({ + _id: apiKeyCreated.doc._id, + }); + + jest.spyOn(apiKeyService, 'findOne').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOne({ + _id: faker.datatype.uuid(), + }); + + jest.spyOn(apiKeyService, 'findOne').mockReturnValueOnce(null); + + expect(result).toBeNull(); + }); + }); + + describe('findOneByKey', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByKey( + apiKeyCreated.doc.key + ); + jest.spyOn(apiKeyService, 'findOneByKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByKey( + '123123123' + ); + + jest.spyOn(apiKeyService, 'findOneByKey').mockReturnValueOnce(null); + + expect(result).toBeFalsy(); + expect(result).toBeNull(); + }); + }); + + describe('findOneByActiveKey', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByActiveKey( + apiKeyCreated.doc.key + ); + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByActiveKey( + '123123123' + ); + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockReturnValueOnce( + null + ); + + expect(result).toBeFalsy(); + expect(result).toBeNull(); + }); + }); + + describe('getTotal', () => { + it('should return total data of apikeys', async () => { + const result: number = await apiKeyService.getTotal({ + name: apiKeyName1, + }); + + jest.spyOn(apiKeyService, 'getTotal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + }); + + describe('active', () => { + it('should make apikey to be active', async () => { + const result: ApiKeyDoc = await apiKeyService.active(apiKey); + + jest.spyOn(apiKeyService, 'active').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.isActive).toBe(true); + }); + }); + + describe('inactive', () => { + it('should make apikey to be inactive', async () => { + const result: ApiKeyDoc = await apiKeyService.inactive(apiKey); + + jest.spyOn(apiKeyService, 'inactive').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.isActive).toBe(false); + }); + }); + + describe('create', () => { + it('should return a new apikeys', async () => { + const result: IApiKeyCreated = await apiKeyService.create({ + name: apiKeyName2, + }); + + jest.spyOn(apiKeyService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName2); + }); + + it('should return a new apikeys with expiration', async () => { + const result: IApiKeyCreated = await apiKeyService.create({ + name: apiKeyName3, + startDate, + endDate, + }); + + jest.spyOn(apiKeyService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName3); + }); + }); + + describe('createRaw', () => { + it('should return a new apikeys', async () => { + const result: IApiKeyCreated = await apiKeyService.createRaw({ + name: apiKeyName3, + description: faker.random.alphaNumeric(), + key: await apiKeyService.createKey(), + secret: await apiKeyService.createSecret(), + }); + + jest.spyOn(apiKeyService, 'createRaw').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName3); + }); + + it('should return a new apikeys with expiration', async () => { + const result: IApiKeyCreated = await apiKeyService.createRaw({ + name: apiKeyName2, + description: faker.random.alphaNumeric(), + key: await apiKeyService.createKey(), + secret: await apiKeyService.createSecret(), + startDate, + endDate, + }); + + jest.spyOn(apiKeyService, 'createRaw').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName2); + }); + }); + + describe('update', () => { + it('should return a updated apikey', async () => { + const nameUpdate = faker.random.alphaNumeric(10); + const result: ApiKeyDoc = await apiKeyService.update(apiKey, { + name: nameUpdate, + description: faker.random.alphaNumeric(20), + }); + jest.spyOn(apiKeyService, 'update').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.name).toBe(nameUpdate); + }); + }); + + describe('updateDate', () => { + it('should return a updated apikey', async () => { + const result: ApiKeyEntity = await apiKeyService.updateDate( + apiKey, + { + startDate, + endDate, + } + ); + jest.spyOn(apiKeyService, 'updateDate').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + }); + }); + + describe('reset', () => { + it('old hashed should be difference with new hashed', async () => { + const hashOld: string = apiKey.hash; + const secret: string = await apiKeyService.createSecret(); + const result: ApiKeyDoc = await apiKeyService.reset(apiKey, secret); + jest.spyOn(apiKeyService, 'reset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.hash).not.toBe(hashOld); + }); + }); + + describe('delete', () => { + it('should be success to delete', async () => { + const result: ApiKeyDoc = await apiKeyService.delete(apiKey); + + jest.spyOn(apiKeyService, 'delete').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + }); + }); + + describe('validateHashApiKey', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.validateHashApiKey( + apiKeyCreated.doc.hash, + apiKeyCreated.doc.hash + ); + jest.spyOn(apiKeyService, 'validateHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await apiKeyService.validateHashApiKey( + apiKeyCreated.doc.hash, + faker.random.alphaNumeric(12) + ); + + jest.spyOn(apiKeyService, 'validateHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('createKey', () => { + it('should return a string', async () => { + const result: string = await apiKeyService.createKey(); + + jest.spyOn(apiKeyService, 'createKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('createSecret', () => { + it('should return a string', async () => { + const result: string = await apiKeyService.createSecret(); + + jest.spyOn(apiKeyService, 'createSecret').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('createKey', () => { + it('should return a hashed string', async () => { + const key1: string = await apiKeyService.createKey(); + const secret1: string = await apiKeyService.createSecret(); + const hashed: string = helperHashService.sha256( + `${key1}:${secret1}` + ); + const result: string = await apiKeyService.createHashApiKey( + key1, + secret1 + ); + + jest.spyOn(apiKeyService, 'createHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(hashed); + }); + }); + + describe('deleteMany', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.deleteMany({ + _id: apiKeyCreated.doc._id, + }); + + jest.spyOn(apiKeyService, 'deleteMany').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('inactiveManyByEndDate', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.inactiveManyByEndDate(); + + jest.spyOn( + apiKeyService, + 'inactiveManyByEndDate' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/auth/auth.service.spec.ts b/test/unit/auth/auth.service.spec.ts new file mode 100644 index 0000000..81f6785 --- /dev/null +++ b/test/unit/auth/auth.service.spec.ts @@ -0,0 +1,692 @@ +import { Test } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import configs from 'src/configs'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { AuthModule } from 'src/common/auth/auth.module'; + +describe('AuthService', () => { + let authService: AuthService; + let configService: ConfigService; + let helperDateService: HelperDateService; + + let encryptedAccessToken: string; + let accessToken: string; + let encryptedRefreshToken: string; + let refreshToken: string; + let encryptedPermissionToken: string; + let permissionToken: string; + + let prefixAuthorization: string; + let accessTokenExpirationTime: number; + let refreshTokenExpirationTime: number; + let refreshTokenExpirationTimeRememberMe: number; + let issuer: string; + let audience: string; + let subject: string; + let payloadEncryption: boolean; + let permissionTokenExpirationTime: number; + + // cSpell:ignore ZfqgaDMPpWQ3lJEGQ8Ueu stnk + const user: Record = { + _id: '623cb7fd37a861a10bac2c91', + isActive: true, + salt: '$2b$08$GZfqgaDMPpWQ3lJEGQ8Ueu', + passwordExpired: new Date('2023-03-24T18:27:09.500Z'), + password: + '$2b$08$GZfqgaDMPpWQ3lJEGQ8Ueu1vJ3C6G3stnkS/5e61bK/4f1.Fuw2Eq', + role: { + _id: '623cb7f7965a74bf7a0e9e53', + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + isActive: true, + permissions: [], + name: 'admin', + }, + email: 'admin@mail.com', + mobileNumber: '08111111111', + lastName: 'test', + firstName: 'admin@mail.com', + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + AuthModule, + ], + }).compile(); + + authService = moduleRef.get(AuthService); + configService = moduleRef.get(ConfigService); + helperDateService = moduleRef.get(HelperDateService); + user.passwordExpired = helperDateService.forwardInDays(30); + + accessToken = await authService.createAccessToken(user); + encryptedAccessToken = await authService.encryptAccessToken(user); + refreshToken = await authService.createRefreshToken(user, { + notBeforeExpirationTime: 0, + }); + encryptedRefreshToken = await authService.encryptRefreshToken(user); + permissionToken = await authService.createPermissionToken(user); + encryptedPermissionToken = await authService.encryptPermissionToken( + user + ); + + prefixAuthorization = configService.get( + 'auth.prefixAuthorization' + ); + accessTokenExpirationTime = configService.get( + 'auth.accessToken.expirationTime' + ); + refreshTokenExpirationTime = configService.get( + 'auth.refreshToken.expirationTime' + ); + refreshTokenExpirationTimeRememberMe = configService.get( + 'auth.refreshToken.expirationTimeRememberMe' + ); + issuer = configService.get('auth.issuer'); + audience = configService.get('auth.audience'); + subject = configService.get('auth.subject'); + payloadEncryption = configService.get( + 'auth.payloadEncryption' + ); + permissionTokenExpirationTime = configService.get( + 'auth.permissionToken.expirationTime' + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', async () => { + expect(authService).toBeDefined(); + }); + + describe('encryptAccessToken', () => { + it('should be return a hashed string', async () => { + const result: string = await authService.encryptAccessToken(user); + + jest.spyOn(authService, 'encryptAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('decryptAccessToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptAccessToken({ + data: encryptedAccessToken, + }); + + jest.spyOn(authService, 'decryptAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createAccessToken', () => { + it('should be create access token in string, from object', async () => { + const result: string = await authService.createAccessToken(user); + + jest.spyOn(authService, 'createAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create access token in string, from string', async () => { + const result: string = await authService.createAccessToken(''); + + jest.spyOn(authService, 'createAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('validateAccessToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validateAccessToken( + accessToken + ); + + jest.spyOn(authService, 'validateAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validateAccessToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn(authService, 'validateAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadAccessToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadAccessToken(accessToken); + + jest.spyOn(authService, 'payloadAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('encryptRefreshToken', () => { + it('should be return a hashed string', async () => { + const result: string = await authService.encryptRefreshToken(user); + + jest.spyOn(authService, 'encryptRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('decryptRefreshToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptRefreshToken({ + data: encryptedRefreshToken, + }); + + jest.spyOn(authService, 'decryptRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createRefreshToken', () => { + it('should be create refresh token in string, from object', async () => { + const result: string = await authService.createRefreshToken(user); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string, from string', async () => { + const result: string = await authService.createRefreshToken(''); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string with options rememberMe, from string', async () => { + const result: string = await authService.createRefreshToken(user, { + rememberMe: true, + }); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('validateRefreshToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validateRefreshToken( + refreshToken + ); + + jest.spyOn(authService, 'validateRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validateRefreshToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn(authService, 'validateRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadRefreshToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadRefreshToken(refreshToken); + + jest.spyOn(authService, 'payloadRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('encryptPermissionToken', () => { + it('should be success', async () => { + const payloadHashedPermissionToken = + await authService.encryptPermissionToken(user); + jest.spyOn( + authService, + 'encryptPermissionToken' + ).mockImplementation(async () => payloadHashedPermissionToken); + + expect(await authService.encryptPermissionToken(user)).toBe( + payloadHashedPermissionToken + ); + }); + }); + + describe('decryptPermissionToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptPermissionToken({ + data: encryptedPermissionToken, + }); + + jest.spyOn( + authService, + 'decryptPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createPermissionToken', () => { + it('should be create refresh token in string, from object', async () => { + const result: string = await authService.createPermissionToken( + user + ); + + jest.spyOn( + authService, + 'createPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string, from string', async () => { + const result: string = await authService.createPermissionToken(''); + + jest.spyOn( + authService, + 'createPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('validatePermissionToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validatePermissionToken( + permissionToken + ); + + jest.spyOn( + authService, + 'validatePermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validatePermissionToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn( + authService, + 'validatePermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadPermissionToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadPermissionToken(permissionToken); + + jest.spyOn( + authService, + 'payloadPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('validateUser', () => { + it('should be a valid user', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + const result: boolean = await authService.validateUser( + password, + passwordHash.passwordHash + ); + + jest.spyOn(authService, 'validateUser').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be not valid user', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + const result: boolean = await authService.validateUser( + 'aasdasd12312', + passwordHash.passwordHash + ); + + jest.spyOn(authService, 'validateUser').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('createPayloadAccessToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadAccessToken(user, false); + + jest.spyOn( + authService, + 'createPayloadAccessToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + + it('payload with login date options', async () => { + const result: Record = + await authService.createPayloadAccessToken(user, false, { + loginDate: new Date(), + }); + + jest.spyOn( + authService, + 'createPayloadAccessToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + expect(result.loginDate).toBeDefined(); + }); + }); + + describe('createPayloadRefreshToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadRefreshToken(user._id, false); + + jest.spyOn( + authService, + 'createPayloadRefreshToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + + it('payload with login date options', async () => { + const result: Record = + await authService.createPayloadRefreshToken(user._id, false, { + loginDate: new Date(), + }); + + jest.spyOn( + authService, + 'createPayloadRefreshToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + expect(result.loginDate).toBeDefined(); + }); + }); + + describe('createPayloadPermissionToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadPermissionToken(user); + + jest.spyOn( + authService, + 'createPayloadPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toEqual(user); + }); + }); + + describe('createPassword', () => { + it('should be success', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const result: IAuthPassword = await authService.createPassword( + password + ); + + jest.spyOn(authService, 'createPassword').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('checkPasswordExpired', () => { + it('should be not expired', async () => { + const result: boolean = await authService.checkPasswordExpired( + user.passwordExpired + ); + jest.spyOn(authService, 'checkPasswordExpired').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + + it('should be expired', async () => { + const expiredDate = new Date('1999-01-01'); + const result: boolean = await authService.checkPasswordExpired( + expiredDate + ); + + jest.spyOn(authService, 'checkPasswordExpired').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('getTokenType', () => { + it('should be success', async () => { + const result: string = await authService.getTokenType(); + + jest.spyOn(authService, 'getTokenType').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(prefixAuthorization); + }); + }); + + describe('getAccessTokenExpirationTime', () => { + it('should be give number in days for access token expiration', async () => { + const result: number = + await authService.getAccessTokenExpirationTime(); + + jest.spyOn( + authService, + 'getAccessTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(accessTokenExpirationTime); + }); + }); + + describe('getRefreshTokenExpirationTime', () => { + it('should be give number in days for refresh token expiration', async () => { + const result: number = + await authService.getRefreshTokenExpirationTime(); + + jest.spyOn( + authService, + 'getRefreshTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(refreshTokenExpirationTime); + }); + + it('should be give number in days for refresh token expiration with long period', async () => { + const result: number = + await authService.getRefreshTokenExpirationTime(true); + + jest.spyOn( + authService, + 'getRefreshTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(refreshTokenExpirationTimeRememberMe); + }); + }); + + describe('getIssuer', () => { + it('should be success', async () => { + const result: string = await authService.getIssuer(); + + jest.spyOn(authService, 'getIssuer').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(issuer); + }); + }); + + describe('getAudience', () => { + it('should be success', async () => { + const result: string = await authService.getAudience(); + + jest.spyOn(authService, 'getAudience').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(audience); + }); + }); + + describe('getSubject', () => { + it('should be success', async () => { + const result: string = await authService.getSubject(); + + jest.spyOn(authService, 'getSubject').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(subject); + }); + }); + + describe('getPayloadEncryption', () => { + it('should be success', async () => { + const result: boolean = await authService.getPayloadEncryption(); + + jest.spyOn(authService, 'getPayloadEncryption').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(payloadEncryption); + }); + }); + + describe('getPermissionTokenExpirationTime', () => { + it('should be success', async () => { + const result: number = + await authService.getPermissionTokenExpirationTime(); + + jest.spyOn( + authService, + 'getPermissionTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(permissionTokenExpirationTime); + }); + }); +}); diff --git a/test/unit/database/database.options.service.spec.ts b/test/unit/database/database.options.service.spec.ts new file mode 100644 index 0000000..7f9e0e6 --- /dev/null +++ b/test/unit/database/database.options.service.spec.ts @@ -0,0 +1,122 @@ +import { ConfigModule } from '@nestjs/config'; +import { MongooseModuleOptions } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; + +describe('DatabaseOptionsService', () => { + let databaseOptionsService: DatabaseOptionsService; + + beforeEach(async () => { + process.env.APP_ENV = 'development'; + process.env.DATABASE_USER = 'nestUser'; + process.env.DATABASE_PASSWORD = 'nestUserTestPassword'; + process.env.DATABASE_DEBUG = 'true'; + process.env.DATABASE_OPTIONS = + 'replicaSet=rs0&retryWrites=true&w=majority'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + + databaseOptionsService = moduleRef.get( + DatabaseOptionsService + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(databaseOptionsService).toBeDefined(); + }); + + describe('createOptions development', () => { + beforeEach(async () => { + process.env.APP_ENV = 'development'; + process.env.DATABASE_USER = 'nestUser'; + process.env.DATABASE_PASSWORD = 'nestUserTestPassword'; + process.env.DATABASE_DEBUG = 'true'; + process.env.DATABASE_OPTIONS = + 'replicaSet=rs0&retryWrites=true&w=majority'; + + await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + }); + + it('should be return mongoose options', async () => { + const result: MongooseModuleOptions = + databaseOptionsService.createOptions(); + + jest.spyOn( + databaseOptionsService, + 'createOptions' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + }); + }); + + describe('createOptions production', () => { + beforeEach(async () => { + process.env.APP_ENV = 'production'; + process.env.DATABASE_OPTIONS = ''; + + await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + }); + + it('should be return mongoose options with production env', async () => { + process.env.APP_ENV = 'production'; + process.env.DATABASE_OPTIONS = ''; + + const result: MongooseModuleOptions = + databaseOptionsService.createOptions(); + + jest.spyOn( + databaseOptionsService, + 'createOptions' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/debugger/debugger.options.service.spec.ts b/test/unit/debugger/debugger.options.service.spec.ts new file mode 100644 index 0000000..dea64c8 --- /dev/null +++ b/test/unit/debugger/debugger.options.service.spec.ts @@ -0,0 +1,199 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { DebuggerOptionsModule } from 'src/common/debugger/debugger.options.module'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; +import winston from 'winston'; + +describe('DebuggerOptionService ', () => { + let debuggerOptionService: DebuggerOptionService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(debuggerOptionService).toBeDefined(); + }); + + describe('createLogger only write into console and file', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into console and file', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(4); + }); + }); + + describe('createLogger only write into console', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'false'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into console ', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(1); + }); + }); + + describe('createLogger only write into file', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'false'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into file', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(3); + }); + }); + + describe('createLogger write file and console inactive', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'false'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'false'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must not write to file or console', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(0); + }); + }); +}); diff --git a/test/unit/debugger/debugger.service.spec.ts b/test/unit/debugger/debugger.service.spec.ts new file mode 100644 index 0000000..b464e03 --- /dev/null +++ b/test/unit/debugger/debugger.service.spec.ts @@ -0,0 +1,174 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { DebuggerModule } from 'src/common/debugger/debugger.module'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; + +describe('DebuggerService', () => { + let debuggerService: DebuggerService; + + const sDescription = 'test description'; + const sClass = 'test class'; + const cFunction = 'test function'; + const data = { test: 'test' }; + + beforeEach(async () => { + process.env.DEBUGGER_HTTP_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_HTTP_WRITE_INTO_FILE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerModule.forRoot(), + ], + }).compile(); + + debuggerService = moduleRef.get(DebuggerService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(debuggerService).toBeDefined(); + }); + + describe('info', () => { + it('should write into log', async () => { + const result: void = debuggerService.info('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'info').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.info( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'info').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('debug', () => { + it('should write into log', async () => { + const result: void = debuggerService.debug('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'debug').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.debug( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'debug').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('warn', () => { + it('should write into log', async () => { + const result: void = debuggerService.warn('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'warn').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.warn( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'warn').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('error', () => { + it('should write into log', async () => { + const result: void = debuggerService.error('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'error').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.error( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'error').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/test/unit/helper/helper.array.service.spec.ts b/test/unit/helper/helper.array.service.spec.ts new file mode 100644 index 0000000..5e70837 --- /dev/null +++ b/test/unit/helper/helper.array.service.spec.ts @@ -0,0 +1,503 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import configs from 'src/configs'; + +describe('HelperArrayService', () => { + let helperArrayService: HelperArrayService; + let arrays: (string | number)[]; + let arraysString: string; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperArrayService = + moduleRef.get(HelperArrayService); + + arrays = [1, '2', '3', 3, 1, 6, 7, 8]; + arraysString = '1,2,3,3,1,6,7,8'; + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperArrayService).toBeDefined(); + }); + + describe('getLeftByIndex', () => { + it('should be return a value by index from left', async () => { + const result: number | string = helperArrayService.getLeftByIndex< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLeftByIndex' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(arrays[1]); + }); + }); + + describe('getRightByIndex', () => { + it('should be return a value by index from right', async () => { + const result: number | string = helperArrayService.getRightByIndex< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getRightByIndex' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(arrays[arrays.length - 1]); + }); + }); + + describe('getLeftByLength', () => { + it('should be return a array by 1 length from left', async () => { + const result: (number | string)[] = + helperArrayService.getLeftByLength(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[0]]); + expect(result[0]).toBe(arrays[0]); + }); + + it('should be return a array by 3 length from left', async () => { + const result: (number | string)[] = + helperArrayService.getLeftByLength(arrays, 3); + + jest.spyOn( + helperArrayService, + 'getLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[0], arrays[1], arrays[2]]); + expect(result[0]).toBe(arrays[0]); + expect(result[2]).toBe(arrays[2]); + }); + }); + + describe('getRightByLength', () => { + it('should be return a array by 1 length from right', async () => { + const result: (number | string)[] = + helperArrayService.getRightByLength(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[arrays.length - 1]]); + expect(result[0]).toBe(arrays[arrays.length - 1]); + }); + + it('should be return a array by 3 length from right', async () => { + const result: (number | string)[] = + helperArrayService.getRightByLength(arrays, 3); + + jest.spyOn( + helperArrayService, + 'getRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([ + arrays[arrays.length - 3], + arrays[arrays.length - 2], + arrays[arrays.length - 1], + ]); + expect(result[0]).toBe(arrays[arrays.length - 3]); + expect(result[2]).toBe(arrays[arrays.length - 1]); + }); + }); + + describe('getLast', () => { + it('should be return a last data of array', async () => { + const result: number | string = helperArrayService.getLast< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'getLast').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toEqual(arrays[arrays.length - 1]); + }); + }); + + describe('getFirst', () => { + it('should be return a first data of array', async () => { + const result: number | string = helperArrayService.getFirst< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'getFirst').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toEqual(arrays[0]); + }); + }); + + describe('getFirstIndexByValue', () => { + it('should be return a first index with search by value', async () => { + const result: number = helperArrayService.getFirstIndexByValue< + number | string + >(arrays, '2'); + + jest.spyOn( + helperArrayService, + 'getFirstIndexByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual(1); + }); + }); + + describe('getLastIndexByValue', () => { + it('should be return a last index with search by value', async () => { + const result: number = helperArrayService.getLastIndexByValue< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLastIndexByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual(4); + }); + }); + + describe('removeByValue', () => { + it('should remove array by searching a value', async () => { + const result: IHelperArrayRemove = + helperArrayService.removeByValue(arrays, 8); + + jest.spyOn(helperArrayService, 'removeByValue').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.removed).toBeDefined(); + expect(result.arrays).toBeDefined(); + expect(result.removed.length).toEqual(1); + expect(result.removed.length).toEqual(1); + expect(result.removed[0]).toEqual(8); + expect(result.arrays.length).toEqual(7); + expect(result.arrays[result.arrays.length - 1]).toBe(7); + }); + }); + + describe('removeLeftByLength', () => { + it('should remove array by length from left', async () => { + const result: (number | string)[] = + helperArrayService.removeLeftByLength( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'removeLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toEqual(arrays.length - 1); + expect(result[0]).toBe('2'); + }); + }); + + describe('removeRightByLength', () => { + it('should remove array by length from right', async () => { + const result: (number | string)[] = + helperArrayService.removeRightByLength( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'removeRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toEqual(arrays.length - 1); + expect(result[result.length - 1]).toBe(7); + }); + }); + + describe('joinToString', () => { + it('should join array to string', async () => { + const result: string = helperArrayService.joinToString< + number | string + >(arrays, ','); + + jest.spyOn(helperArrayService, 'joinToString').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(arraysString); + }); + }); + + describe('reverse', () => { + it('array should be reversed', async () => { + const result: (number | string)[] = helperArrayService.reverse< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'reverse').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result[0]).toBe(8); + expect(result[result.length - 1]).toBe(1); + }); + }); + + describe('unique', () => { + it('array should be unique', async () => { + const result: (number | string)[] = helperArrayService.unique< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'unique').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(7); + }); + }); + + describe('shuffle', () => { + it('array should be shuffle', async () => { + const result: (number | string)[] = helperArrayService.shuffle< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'shuffle').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(arrays.length); + expect(result.every((val) => arrays.includes(val))).toBe(true); + expect(result.every((val, idx) => arrays[idx] === val)).toBe(false); + }); + }); + + describe('merge', () => { + it('array should be merger', async () => { + const result: (number | string)[] = helperArrayService.merge< + number | string + >(arrays, arrays); + + jest.spyOn(helperArrayService, 'merge').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(arrays.length * 2); + }); + }); + + describe('mergeUnique', () => { + it('array should be merger and unique array', async () => { + const result: (number | string)[] = helperArrayService.mergeUnique< + number | string + >(arrays, arrays); + + jest.spyOn(helperArrayService, 'mergeUnique').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(7); + }); + }); + + describe('filterIncludeByValue', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterIncludeByValue( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'filterIncludeByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(2); + expect(result.every((val) => val === 1)).toBe(true); + }); + }); + + describe('filterNotIncludeByValue', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterNotIncludeByValue( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'filterNotIncludeByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(6); + expect(result.every((val) => val !== 1)).toBe(true); + }); + }); + + describe('filterNotIncludeUniqueByArray', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterNotIncludeUniqueByArray< + number | string + >(arrays, [1]); + + jest.spyOn( + helperArrayService, + 'filterNotIncludeUniqueByArray' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(6); + expect(result.every((val) => val !== 1)).toBe(true); + }); + }); + + describe('filterIncludeUniqueByArray', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterIncludeUniqueByArray( + arrays, + [1] + ); + + jest.spyOn( + helperArrayService, + 'filterIncludeUniqueByArray' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(1); + expect(result.every((val) => val === 1)).toBe(true); + }); + }); + + describe('equals', () => { + it('should be equals', async () => { + const result: boolean = helperArrayService.equals(arrays, arrays); + + jest.spyOn(helperArrayService, 'equals').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('notEquals', () => { + it('should be equals', async () => { + const result: boolean = helperArrayService.notEquals( + arrays, + [1, 2, 3] + ); + + jest.spyOn(helperArrayService, 'notEquals').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('in', () => { + it('value must in arrays', async () => { + const result: boolean = helperArrayService.in(arrays, [1]); + + jest.spyOn(helperArrayService, 'in').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('notIn', () => { + it('value must not in arrays', async () => { + const result: boolean = helperArrayService.notIn(arrays, ['z']); + + jest.spyOn(helperArrayService, 'notIn').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('includes', () => { + it('value must includes arrays', async () => { + const result: boolean = helperArrayService.includes(arrays, 1); + + jest.spyOn(helperArrayService, 'includes').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('chunk', () => { + it('array chunk to be x value', async () => { + const result: (number | string)[][] = helperArrayService.chunk( + arrays, + 2 + ); + + jest.spyOn(helperArrayService, 'chunk').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(4); + }); + }); +}); diff --git a/test/unit/helper/helper.date.service.spec.ts b/test/unit/helper/helper.date.service.spec.ts new file mode 100644 index 0000000..f83b241 --- /dev/null +++ b/test/unit/helper/helper.date.service.spec.ts @@ -0,0 +1,974 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, +} from 'src/common/helper/constants/helper.enum.constant'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + IHelperDateExtractDate, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import configs from 'src/configs'; + +describe('HelperDateService', () => { + let helperDateService: HelperDateService; + const dateString = '2000-01-01'; + const date1: Date = new Date('2000-01-01'); + const date2: Date = new Date('2010-01-10'); + const dateTimestamp: number = date2.valueOf(); + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperDateService = moduleRef.get(HelperDateService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperDateService).toBeDefined(); + }); + + describe('calculateAge', () => { + it('return age in number', async () => { + const result: number = helperDateService.calculateAge(date1); + + jest.spyOn(helperDateService, 'calculateAge').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('diff', () => { + it('should be return a number of days differences', async () => { + const result: number = helperDateService.diff(date1, date2); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.MINUTES, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.HOURS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes days', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.DAYS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of seconds differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.SECONDS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of milis differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.MILIS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('check', () => { + it('string must be a valid date', async () => { + const result: boolean = helperDateService.check(dateString); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('date must be a valid date', async () => { + const result: boolean = helperDateService.check(date1); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('number must be a valid date', async () => { + const result: boolean = helperDateService.check(dateTimestamp); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkTimestamp', () => { + it('number must be a valid timestamp', async () => { + const result: boolean = + helperDateService.checkTimestamp(dateTimestamp); + + jest.spyOn(helperDateService, 'checkTimestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('create', () => { + it('should be create date base on today', async () => { + const result: Date = helperDateService.create(); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be create date base on date parameter', async () => { + const result: Date = helperDateService.create(date1); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be create date base on today and force the time to start of day', async () => { + const result: Date = helperDateService.create(null, { + startOfDay: true, + }); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('timestamp', () => { + it('should be create timestamp base on today', async () => { + const result: number = helperDateService.timestamp(); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be create timestamp base on date parameter', async () => { + const result: number = helperDateService.timestamp(date1); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be create timestamp base on today and force the time to start of day', async () => { + const result: number = helperDateService.timestamp(null, { + startOfDay: true, + }); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('format', () => { + it('should be return a day as string, default', async () => { + const result: string = helperDateService.format(date1); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01-01'); + }); + + it('should be return a day as string, format date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01-01'); + }); + + it('should be return a day as string, format friendly date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format friendly date time', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE_TIME, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format year and month only', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.YEAR_MONTH, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01'); + }); + + it('should be return a day as string, format month and date only', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MONTH_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01-01'); + }); + + it('should be return a day as string, format only year', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_YEAR, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000'); + }); + + it('should be return a day as string, format only month', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_MONTH, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01'); + }); + + it('should be return a day as string, format only date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01'); + }); + + it('should be return a day as string, format iso date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ISO_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only day and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DAY_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only day and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DAY_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only hour and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.HOUR_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only hour and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.HOUR_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only minute and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MINUTE_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only minute and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MINUTE_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only second and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.SECOND_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only second and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.SECOND_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMilliseconds', () => { + it('should be forward to 2 milis from today', async () => { + const result: Date = helperDateService.forwardInMilliseconds(2); + + jest.spyOn( + helperDateService, + 'forwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 milis from x date', async () => { + const result: Date = helperDateService.forwardInMilliseconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMilliseconds', () => { + it('should be backward to 2 milis from today', async () => { + const result: Date = helperDateService.backwardInMilliseconds(2); + + jest.spyOn( + helperDateService, + 'backwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 milis from x date', async () => { + const result: Date = helperDateService.backwardInMilliseconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInSeconds', () => { + it('should be forward to 2 seconds from today', async () => { + const result: Date = helperDateService.forwardInSeconds(2); + + jest.spyOn( + helperDateService, + 'forwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 seconds from x date', async () => { + const result: Date = helperDateService.forwardInSeconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInSeconds', () => { + it('should be backward to 2 seconds from today', async () => { + const result: Date = helperDateService.backwardInSeconds(2); + + jest.spyOn( + helperDateService, + 'backwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 seconds from x date', async () => { + const result: Date = helperDateService.backwardInSeconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMinutes', () => { + it('should be forward to 2 minutes from today', async () => { + const result: Date = helperDateService.forwardInMinutes(2); + + jest.spyOn( + helperDateService, + 'forwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 minutes from x date', async () => { + const result: Date = helperDateService.forwardInMinutes(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMinutes', () => { + it('should be backward to 2 minutes from today', async () => { + const result: Date = helperDateService.backwardInMinutes(2); + + jest.spyOn( + helperDateService, + 'backwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 minutes from x date', async () => { + const result: Date = helperDateService.backwardInMinutes(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInHours', () => { + it('should be forward to 2 hours from today', async () => { + const result: Date = helperDateService.forwardInHours(2); + + jest.spyOn(helperDateService, 'forwardInHours').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 hours from x date', async () => { + const result: Date = helperDateService.forwardInHours(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'forwardInHours').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInHours', () => { + it('should be backward to 2 hours from today', async () => { + const result: Date = helperDateService.backwardInHours(2); + + jest.spyOn( + helperDateService, + 'backwardInHours' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 hours from x date', async () => { + const result: Date = helperDateService.backwardInHours(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInHours' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInDays', () => { + it('should be forward to 2 days from today', async () => { + const result: Date = helperDateService.forwardInDays(2); + + jest.spyOn(helperDateService, 'forwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 days from x date', async () => { + const result: Date = helperDateService.forwardInDays(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'forwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInDays', () => { + it('should be backward to 2 days from today', async () => { + const result: Date = helperDateService.backwardInDays(2); + + jest.spyOn(helperDateService, 'backwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 days from x date', async () => { + const result: Date = helperDateService.backwardInDays(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'backwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMonths', () => { + it('should be forward to 2 months from today', async () => { + const result: Date = helperDateService.forwardInMonths(2); + + jest.spyOn( + helperDateService, + 'forwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 months from x date', async () => { + const result: Date = helperDateService.forwardInMonths(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMonths', () => { + it('should be backward to 2 months from today', async () => { + const result: Date = helperDateService.backwardInMonths(2); + + jest.spyOn( + helperDateService, + 'backwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 months from x date', async () => { + const result: Date = helperDateService.backwardInMonths(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfMonth', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfMonth(); + + jest.spyOn(helperDateService, 'endOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfMonth(date1); + + jest.spyOn(helperDateService, 'endOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfMonth', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfMonth(); + + jest.spyOn(helperDateService, 'startOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfMonth(date1); + + jest.spyOn(helperDateService, 'startOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfYear', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfYear(); + + jest.spyOn(helperDateService, 'endOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfYear(date1); + + jest.spyOn(helperDateService, 'endOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfYear', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfYear(); + + jest.spyOn(helperDateService, 'startOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfYear(date1); + + jest.spyOn(helperDateService, 'startOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfDay', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfDay(); + + jest.spyOn(helperDateService, 'endOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfDay(date1); + + jest.spyOn(helperDateService, 'endOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfDay', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfDay(); + + jest.spyOn(helperDateService, 'startOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfDay(date1); + + jest.spyOn(helperDateService, 'startOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('extractDate', () => { + it('should extract a date to dat, month, and year as number', async () => { + const result: IHelperDateExtractDate = + helperDateService.extractDate(date1); + + jest.spyOn(helperDateService, 'extractDate').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.day).toBe('01'); + expect(result.month).toBe('01'); + expect(result.year).toBe('2000'); + }); + }); + + describe('roundDown', () => { + it('should be round down a milis from date', async () => { + const result: Date = helperDateService.roundDown(date1); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and hours from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: true, + minute: false, + second: false, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and minutes from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: false, + minute: true, + second: false, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and seconds from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: false, + minute: false, + second: true, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('getStartAndEndDate', () => { + it('should be success', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate(); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create(); + const start = helperDateService.startOfYear(dt); + const end = helperDateService.endOfYear(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + + it('should be success options year', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate({ year: 2022 }); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create('2022-01-02'); + const start = helperDateService.startOfYear(dt); + const end = helperDateService.endOfYear(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + + it('should be success options year and month', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate({ year: 2022, month: 2 }); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create('2022-02-02'); + const start = helperDateService.startOfMonth(dt); + const end = helperDateService.endOfMonth(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + }); +}); diff --git a/test/unit/helper/helper.encryption.service.spec.ts b/test/unit/helper/helper.encryption.service.spec.ts new file mode 100644 index 0000000..4dd7f13 --- /dev/null +++ b/test/unit/helper/helper.encryption.service.spec.ts @@ -0,0 +1,250 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; +import configs from 'src/configs'; + +describe('HelperEncryptionService', () => { + let helperEncryptionService: HelperEncryptionService; + const audience = 'https://example.com'; + const issuer = 'nest'; + const subject = 'nest'; + const data = 'aaaa'; + const dataObject = { test: 'aaaa' }; + let enBase64: string; + let enAes256: string; + let enAes256Object: string; + let enJwt: string; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperEncryptionService = moduleRef.get( + HelperEncryptionService + ); + + enBase64 = helperEncryptionService.base64Encrypt(data); + enAes256 = helperEncryptionService.aes256Encrypt( + data, + '1234567', + '1231231231231231' + ); + enAes256Object = helperEncryptionService.aes256Encrypt( + dataObject, + '1234567', + '1231231231231231' + ); + enJwt = helperEncryptionService.jwtEncrypt( + { data }, + { expiredIn: '1h', secretKey: data, audience, issuer, subject } + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperEncryptionService).toBeDefined(); + }); + + describe('base64Encrypt', () => { + it('string must encode to base64 string', async () => { + const result: string = helperEncryptionService.base64Encrypt(data); + + jest.spyOn( + helperEncryptionService, + 'base64Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('base64Decrypt', () => { + it('base64 string must decode string', async () => { + const result: string = + helperEncryptionService.base64Decrypt(enBase64); + + jest.spyOn( + helperEncryptionService, + 'base64Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('base64Compare', () => { + it('should be success', async () => { + const result: boolean = helperEncryptionService.base64Compare( + enBase64, + enBase64 + ); + + jest.spyOn( + helperEncryptionService, + 'base64Compare' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = helperEncryptionService.base64Compare( + data, + enBase64 + ); + + jest.spyOn( + helperEncryptionService, + 'base64Compare' + ).mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('aes256Encrypt', () => { + it('string must be encode to aes256 string', async () => { + const result: string = helperEncryptionService.aes256Encrypt( + data, + '1234567', + '1231231231231231' + ); + + jest.spyOn( + helperEncryptionService, + 'aes256Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('object must be encode to aes256 string', async () => { + const result: string = helperEncryptionService.aes256Encrypt( + dataObject, + '1234567', + '1231231231231231' + ); + + jest.spyOn( + helperEncryptionService, + 'aes256Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('aes256Decrypt', () => { + it('aes256 string decode to string', async () => { + const result: string = helperEncryptionService.aes256Decrypt( + enAes256, + '1234567', + '1231231231231231' + ) as string; + + jest.spyOn( + helperEncryptionService, + 'aes256Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be success object', async () => { + const result: Record = + helperEncryptionService.aes256Decrypt( + enAes256Object, + '1234567', + '1231231231231231' + ) as Record; + + jest.spyOn( + helperEncryptionService, + 'aes256Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtEncrypt', () => { + it('should be success to jwt encode string', async () => { + const result: string = helperEncryptionService.jwtEncrypt( + { data }, + { expiredIn: '1h', secretKey: data, audience, issuer, subject } + ); + + jest.spyOn( + helperEncryptionService, + 'jwtEncrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtDecrypt', () => { + it('jwt encode string decode and give a payload', async () => { + const result: Record = + helperEncryptionService.jwtDecrypt(enJwt); + + jest.spyOn( + helperEncryptionService, + 'jwtDecrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtVerify', () => { + it('should be success', async () => { + const result: boolean = helperEncryptionService.jwtVerify(enJwt, { + secretKey: data, + audience, + issuer, + subject, + }); + + jest.spyOn( + helperEncryptionService, + 'jwtVerify' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be failed', async () => { + const result: boolean = helperEncryptionService.jwtVerify(enJwt, { + secretKey: faker.random.alpha(5), + audience, + issuer, + subject, + }); + + jest.spyOn( + helperEncryptionService, + 'jwtVerify' + ).mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + }); + }); +}); diff --git a/test/unit/helper/helper.file.service.spec.ts b/test/unit/helper/helper.file.service.spec.ts new file mode 100644 index 0000000..fc68429 --- /dev/null +++ b/test/unit/helper/helper.file.service.spec.ts @@ -0,0 +1,112 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IHelperFileRows } from 'src/common/helper/interfaces/helper.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import configs from 'src/configs'; +import { WorkBook } from 'xlsx'; + +describe('HelperFileService', () => { + let helperFileService: HelperFileService; + let workbook: WorkBook; + let file: Buffer; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperFileService = moduleRef.get(HelperFileService); + + workbook = helperFileService.createExcelWorkbook([ + { test: 1 }, + { test: 2 }, + ]); + file = helperFileService.writeExcelToBuffer(workbook); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperFileService).toBeDefined(); + }); + + describe('createExcelWorkbook', () => { + it('should be convert array of object to excel workbook', async () => { + const result: WorkBook = helperFileService.createExcelWorkbook([ + { test: 1 }, + { test: 2 }, + ]); + + jest.spyOn( + helperFileService, + 'createExcelWorkbook' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be convert empty array to excel workbook', async () => { + const result: WorkBook = helperFileService.createExcelWorkbook([]); + + jest.spyOn( + helperFileService, + 'createExcelWorkbook' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('writeExcelToBuffer', () => { + it('write workbook to buffer', async () => { + const result: Buffer = + helperFileService.writeExcelToBuffer(workbook); + + jest.spyOn( + helperFileService, + 'writeExcelToBuffer' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('readExcelFromBuffer', () => { + it('should be success', async () => { + const result: IHelperFileRows[] = + helperFileService.readExcelFromBuffer(file); + + jest.spyOn( + helperFileService, + 'readExcelFromBuffer' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('convertToBytes', () => { + it('should be success', async () => { + const result: number = helperFileService.convertToBytes('1mb'); + + jest.spyOn(helperFileService, 'convertToBytes').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1048576); + }); + }); +}); diff --git a/test/unit/helper/helper.geo.service.spec.ts b/test/unit/helper/helper.geo.service.spec.ts new file mode 100644 index 0000000..2ac7924 --- /dev/null +++ b/test/unit/helper/helper.geo.service.spec.ts @@ -0,0 +1,54 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperGeoService } from 'src/common/helper/services/helper.geo.service'; +import configs from 'src/configs'; + +describe('HelperGeoService', () => { + let helperGeoService: HelperGeoService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperGeoService = moduleRef.get(HelperGeoService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(HelperGeoService).toBeDefined(); + }); + + describe('inRadius', () => { + it('should be success', async () => { + const result: boolean = helperGeoService.inRadius( + { + longitude: 6.1754, + latitude: 106.8272, + radiusInMeters: 10, + }, + { longitude: 6.1754, latitude: 106.8272 } + ); + + jest.spyOn(helperGeoService, 'inRadius').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/helper/helper.hash.service.spec.ts b/test/unit/helper/helper.hash.service.spec.ts new file mode 100644 index 0000000..270b8ef --- /dev/null +++ b/test/unit/helper/helper.hash.service.spec.ts @@ -0,0 +1,129 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import configs from 'src/configs'; + +describe('HelperHashService', () => { + let helperHashService: HelperHashService; + const data = 'aaaa'; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperHashService = moduleRef.get(HelperHashService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperHashService).toBeDefined(); + }); + + describe('randomSalt', () => { + it('should be success', async () => { + const result: string = helperHashService.randomSalt(10); + + jest.spyOn(helperHashService, 'randomSalt').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('bcrypt', () => { + it('should be success', async () => { + const salt = helperHashService.randomSalt(10); + const result: string = helperHashService.bcrypt(data, salt); + + jest.spyOn(helperHashService, 'bcrypt').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.startsWith(salt)).toBe(true); + }); + }); + + describe('bcryptCompare', () => { + it('should be success', async () => { + const salt = helperHashService.randomSalt(10); + const hash = helperHashService.bcrypt(data, salt); + const result: boolean = helperHashService.bcryptCompare(data, hash); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const salt = helperHashService.randomSalt(10); + const hash = helperHashService.bcrypt(data, salt); + const result: boolean = helperHashService.bcryptCompare( + 'bbbb', + hash + ); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('sha256', () => { + it('should be success', async () => { + const result: string = helperHashService.sha256(data); + + jest.spyOn(helperHashService, 'sha256').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('sha256Compare', () => { + it('should be success', async () => { + const hash = helperHashService.sha256(data); + const result: boolean = helperHashService.sha256Compare(hash, hash); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const hash = helperHashService.sha256(data); + const result: boolean = helperHashService.sha256Compare( + 'bbbb', + hash + ); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); +}); diff --git a/test/unit/helper/helper.number.service.spec.ts b/test/unit/helper/helper.number.service.spec.ts new file mode 100644 index 0000000..92a5a17 --- /dev/null +++ b/test/unit/helper/helper.number.service.spec.ts @@ -0,0 +1,113 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import configs from 'src/configs'; + +describe('HelperNumberService', () => { + let helperNumberService: HelperNumberService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperNumberService = + moduleRef.get(HelperNumberService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperNumberService).toBeDefined(); + }); + + describe('check', () => { + it('should be success', async () => { + const result: boolean = helperNumberService.check('111'); + + jest.spyOn(helperNumberService, 'check').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('create', () => { + it('should be success', async () => { + const result: number = helperNumberService.create('111'); + + jest.spyOn(helperNumberService, 'create').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(111); + }); + }); + + describe('random', () => { + it('should be success', async () => { + const result: number = helperNumberService.random(10); + + jest.spyOn(helperNumberService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('randomInRange', () => { + it('should be success', async () => { + const result: number = helperNumberService.randomInRange(5, 8); + + jest.spyOn( + helperNumberService, + 'randomInRange' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('percent', () => { + it('should be called', async () => { + const test = jest.spyOn(helperNumberService, 'percent'); + + helperNumberService.percent(5, 8); + expect(test).toHaveBeenCalledWith(5, 8); + }); + + it('should be success with NaN or Infinite', async () => { + const result = helperNumberService.percent(0, 0); + jest.spyOn(helperNumberService, 'percent').mockImplementation( + () => result + ); + + expect(helperNumberService.percent(5, 8)).toBe(result); + }); + + it('should be success', async () => { + const result = helperNumberService.percent(5, 8); + jest.spyOn(helperNumberService, 'percent').mockImplementation( + () => result + ); + + expect(helperNumberService.percent(5, 8)).toBe(result); + }); + }); +}); diff --git a/test/unit/helper/helper.string.service.spec.ts b/test/unit/helper/helper.string.service.spec.ts new file mode 100644 index 0000000..54a56ae --- /dev/null +++ b/test/unit/helper/helper.string.service.spec.ts @@ -0,0 +1,229 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import configs from 'src/configs'; + +describe('HelperStringService', () => { + let helperStringService: HelperStringService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperStringService = + moduleRef.get(HelperStringService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperStringService).toBeDefined(); + }); + + describe('checkEmail', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkEmail('111@mail.com'); + + jest.spyOn(helperStringService, 'checkEmail').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = helperStringService.checkEmail('111'); + + jest.spyOn(helperStringService, 'checkEmail').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('randomReference', () => { + it('should be success', async () => { + const result: string = helperStringService.randomReference(10); + + jest.spyOn( + helperStringService, + 'randomReference' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be success with prefix', async () => { + const result: string = helperStringService.randomReference( + 10, + 'test' + ); + + jest.spyOn( + helperStringService, + 'randomReference' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.startsWith('test')).toBe(true); + }); + }); + + describe('random', () => { + it('should be success', async () => { + const result = helperStringService.random(5); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be success with options prefix', async () => { + const result = helperStringService.random(5, { prefix: 'aaa' }); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.startsWith('aaa')).toBe(true); + }); + + it('should be success with options prefix, safe, and uppercase', async () => { + const result = helperStringService.random(5, { + prefix: 'aaa', + safe: true, + upperCase: true, + }); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.startsWith('AAA')).toBe(true); + }); + }); + + describe('censor', () => { + it('string length 1 should be success', async () => { + const result: string = helperStringService.censor('1'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length 1 - 4 should be success', async () => { + const result: string = helperStringService.censor('125'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length 4 - 10 should be success', async () => { + const result: string = helperStringService.censor('123245'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length > 10 should be success', async () => { + const result: string = helperStringService.censor( + '12312312312312312312312' + ); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('checkPasswordWeak', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordWeak('aaAAbbBBccCC'); + + jest.spyOn( + helperStringService, + 'checkPasswordWeak' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkPasswordMedium', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordMedium('aaAA12345'); + + jest.spyOn( + helperStringService, + 'checkPasswordMedium' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkPasswordStrong', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordStrong('aaAA12345@!'); + + jest.spyOn( + helperStringService, + 'checkPasswordStrong' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkSafeString', () => { + it('should be success', async () => { + const result: boolean = helperStringService.checkSafeString('123'); + + jest.spyOn( + helperStringService, + 'checkSafeString' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/jest.json b/test/unit/jest.json new file mode 100644 index 0000000..3f155c6 --- /dev/null +++ b/test/unit/jest.json @@ -0,0 +1,49 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/unit/api-key/*.spec.ts", + "/test/unit/auth/*.spec.ts", + "/test/unit/config/*.spec.ts", + "/test/unit/database/*.spec.ts", + "/test/unit/debugger/*.spec.ts", + "/test/unit/helper/*.spec.ts", + "/test/unit/logger/*.spec.ts", + "/test/unit/message/*.spec.ts", + "/test/unit/pagination/*.spec.ts", + "/test/unit/setting/*.spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage", + "collectCoverageFrom": [ + "./src/common/api-key/services/**", + "./src/common/auth/services/**", + "./src/common/database/services/**", + "./src/common/debugger/services/**", + "./src/common/helper/services/**", + "./src/common/logger/services/**", + "./src/common/message/services/**", + "./src/common/pagination/services/**", + "./src/common/setting/services/**" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/unit/logger/logger.service.spec.ts b/test/unit/logger/logger.service.spec.ts new file mode 100644 index 0000000..dff4add --- /dev/null +++ b/test/unit/logger/logger.service.spec.ts @@ -0,0 +1,249 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { LoggerCreateDto } from 'src/common/logger/dtos/logger.create.dto'; +import { LoggerModule } from 'src/common/logger/logger.module'; +import { LoggerEntity } from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerService } from 'src/common/logger/services/logger.service'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import configs from 'src/configs'; + +describe('LoggerService', () => { + let apiKeyService: ApiKeyService; + let loggerService: LoggerService; + + let apiKey: IApiKeyCreated; + + const loggerLevel: ENUM_LOGGER_LEVEL = ENUM_LOGGER_LEVEL.INFO; + const logger: LoggerCreateDto = { + action: ENUM_LOGGER_ACTION.TEST, + description: 'test aaa', + method: ENUM_REQUEST_METHOD.GET, + tags: [], + path: '/path', + }; + + const loggerComplete: LoggerCreateDto = { + action: ENUM_LOGGER_ACTION.TEST, + description: 'test aaa', + user: DatabaseDefaultUUID(), + apiKey: DatabaseDefaultUUID(), + requestId: DatabaseDefaultUUID(), + role: DatabaseDefaultUUID(), + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + method: ENUM_REQUEST_METHOD.GET, + statusCode: 10000, + bodies: { + test: 'aaa', + }, + params: { + test: 'bbb', + }, + path: '/path-complete', + tags: [], + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + LoggerModule, + ApiKeyModule, + ], + }).compile(); + + loggerService = moduleRef.get(LoggerService); + apiKeyService = moduleRef.get(ApiKeyService); + + const apiKeyCreate = { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + }; + apiKey = await apiKeyService.create(apiKeyCreate); + + loggerComplete.apiKey = apiKey.doc._id; + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ _id: apiKey.doc._id }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(loggerService).toBeDefined(); + }); + + describe('info', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.info(logger); + + jest.spyOn(loggerService, 'info').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.INFO); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.info( + loggerComplete + ); + + jest.spyOn(loggerService, 'info').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.INFO); + }); + }); + + describe('debug', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.debug(logger); + + jest.spyOn(loggerService, 'debug').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.DEBUG); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.debug( + loggerComplete + ); + + jest.spyOn(loggerService, 'debug').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.DEBUG); + }); + }); + + describe('warning', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.warn(logger); + + jest.spyOn(loggerService, 'warn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.WARM); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.warn( + loggerComplete + ); + + jest.spyOn(loggerService, 'warn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.WARM); + }); + }); + + describe('fatal', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.fatal(logger); + + jest.spyOn(loggerService, 'fatal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.FATAL); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.fatal( + loggerComplete + ); + + jest.spyOn(loggerService, 'fatal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.FATAL); + }); + }); + + describe('raw', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.raw({ + level: loggerLevel, + ...logger, + }); + + jest.spyOn(loggerService, 'raw').mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(loggerLevel); + }); + + it('should be success complete', async () => { + const result: LoggerEntity = await loggerService.raw({ + level: loggerLevel, + ...loggerComplete, + }); + + jest.spyOn(loggerService, 'raw').mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(loggerLevel); + }); + }); +}); diff --git a/test/unit/message/message.service.spec.ts b/test/unit/message/message.service.spec.ts new file mode 100644 index 0000000..46ff1cb --- /dev/null +++ b/test/unit/message/message.service.spec.ts @@ -0,0 +1,481 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { ValidationError } from 'class-validator'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; +import { MessageModule } from 'src/common/message/message.module'; +import { MessageService } from 'src/common/message/services/message.service'; +import configs from 'src/configs'; + +describe('MessageService', () => { + let messageService: MessageService; + + let validationError: ValidationError[]; + let validationErrorTwo: ValidationError[]; + let validationErrorThree: ValidationError[]; + let validationErrorConstrainEmpty: ValidationError[]; + let validationErrorImport: IValidationErrorImport[]; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + MessageModule, + ], + }).compile(); + + messageService = moduleRef.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' }, + }, + ]; + + validationErrorTwo = [ + { + 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: [], + }, + ], + }, + ]; + + validationErrorThree = [ + { + 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: [], + }, + ], + }, + ], + }, + ]; + + validationErrorConstrainEmpty = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + children: [], + }, + ]; + + validationErrorImport = [ + { + row: 0, + file: 'error.xlsx', + 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, + file: 'error.xlsx', + 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, + file: 'error.xlsx', + 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', + }, + }, + { + target: { + number: null, + area: 'area', + city: 'area timur', + address: 'address 3', + tags: ['test'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 3, + file: 'error.xlsx', + 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', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 4, + file: 'error.xlsx', + 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, + file: 'error.xlsx', + 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', + }, + }, + ], + }, + ]; + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(messageService).toBeDefined(); + }); + + describe('getAvailableLanguages', () => { + it('should be success', async () => { + const result: string[] = + await messageService.getAvailableLanguages(); + + jest.spyOn( + messageService, + 'getAvailableLanguages' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBeTruthy(); + }); + }); + + + describe('get', () => { + it('should be success', async () => { + const result: string | IMessage = await messageService.get( + 'test.hello' + ); + + jest.spyOn(messageService, 'get').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('getRequestErrorsMessage', () => { + it('single message should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + 'en', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('multi message should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + 'en', + 'id', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('multi message if there has some undefined value should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + undefined, + 'id', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('two children should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorTwo + ); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('three children should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorThree + ); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('empty constrain should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorConstrainEmpty + ); + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('getImportErrorsMessage', () => { + it('should be success', async () => { + const result: IErrorsImport[] = + await messageService.getImportErrorsMessage( + validationErrorImport + ); + + jest.spyOn( + messageService, + 'getImportErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success with options', async () => { + const result: IErrorsImport[] = + await messageService.getImportErrorsMessage( + validationErrorImport, + ['en', 'id'] + ); + + jest.spyOn( + messageService, + 'getImportErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/pagination/pagination.service.spec.ts b/test/unit/pagination/pagination.service.spec.ts new file mode 100644 index 0000000..28868c2 --- /dev/null +++ b/test/unit/pagination/pagination.service.spec.ts @@ -0,0 +1,423 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + PAGINATION_AVAILABLE_ORDER_BY, + PAGINATION_MAX_PAGE, + PAGINATION_MAX_PER_PAGE, + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_PAGE, + PAGINATION_PER_PAGE, +} from 'src/common/pagination/constants/pagination.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationModule } from 'src/common/pagination/pagination.module'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import configs from 'src/configs'; + +describe('PaginationService', () => { + let paginationService: PaginationService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + PaginationModule, + ], + }).compile(); + + paginationService = moduleRef.get(PaginationService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(paginationService).toBeDefined(); + }); + + describe('offset', () => { + it('should be offset for page 1', async () => { + const result: number = paginationService.offset(1, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(0); + }); + + it('should be offset for page 2', async () => { + const result: number = paginationService.offset(2, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be offset for page 100', async () => { + const result: number = paginationService.offset(10, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(90); + }); + + it('should be offset with max perPage', async () => { + const result: number = paginationService.offset(2, 150); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(100); + }); + + it('should be offset with max page', async () => { + const result: number = paginationService.offset(50, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(190); + }); + }); + + describe('totalPage', () => { + it('should be return a total page base on perPage and total data', async () => { + const result: number = paginationService.totalPage(100, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be return a 1 because total data is zero', async () => { + const result: number = paginationService.totalPage(0, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(1); + }); + + it('should be return a max total page ', async () => { + const result: number = paginationService.totalPage(10000, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(20); + }); + }); + + describe('offsetWithoutMax', () => { + it('should be offset for page 1', async () => { + const result: number = paginationService.offsetWithoutMax(1, 10); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(0); + }); + + it('should be return offset without depends on max page', async () => { + const result: number = paginationService.offsetWithoutMax(50, 10); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(490); + }); + + it('should be return offset without depends on max per page', async () => { + const result: number = paginationService.offsetWithoutMax(2, 200); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(200); + }); + }); + + describe('totalPageWithoutMax', () => { + it('should be return a total page base on perPage and total data', async () => { + const result: number = paginationService.totalPageWithoutMax( + 100, + 10 + ); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be return a 1 because total data is zero', async () => { + const result: number = paginationService.totalPageWithoutMax(0, 10); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(1); + }); + + it('should be return a total page without depends on max page ', async () => { + const result: number = paginationService.totalPageWithoutMax( + 10000, + 10 + ); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(1000); + }); + }); + + describe('page', () => { + it('should be return page', async () => { + const result: number = paginationService.page(1); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + + it('page 0, should convert to default perPage', async () => { + const result: number = paginationService.page(0); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_PAGE); + }); + + it('page more than max, should return max page', async () => { + const result: number = paginationService.page( + PAGINATION_MAX_PAGE + 10 + ); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_MAX_PAGE); + }); + }); + + describe('perPage', () => { + it('should be return perPage', async () => { + const result: number = paginationService.perPage(1); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + + it('perPage 0, should convert to default perPage', async () => { + const result: number = paginationService.perPage(0); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_PER_PAGE); + }); + + it('perPage more than max, should return max perPage', async () => { + const result: number = paginationService.perPage( + PAGINATION_MAX_PER_PAGE + 10 + ); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_MAX_PER_PAGE); + }); + }); + + describe('order', () => { + it('should be return order with null parameter', async () => { + const result: IPaginationOrder = paginationService.order(); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return order with unallow order by value', async () => { + const result: IPaginationOrder = paginationService.order('status'); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return order', async () => { + const result: IPaginationOrder = paginationService.order( + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_AVAILABLE_ORDER_BY + ); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('search', () => { + it('should be return search', async () => { + const result: Record = + paginationService.search('test', ['name']); + + jest.spyOn(paginationService, 'search').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return undefined', async () => { + const result: Record = + paginationService.search(undefined, ['name']); + + jest.spyOn(paginationService, 'search').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + }); + }); + + describe('filterEqual', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterEqual('name', 'test'); + + jest.spyOn(paginationService, 'filterEqual').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result['name']).toBe('test'); + }); + }); + + describe('filterContain', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterContain('name', 'test'); + + jest.spyOn(paginationService, 'filterContain').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterContainFullMatch', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterContainFullMatch('name', 'test'); + + jest.spyOn( + paginationService, + 'filterContainFullMatch' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterIn', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterIn('name', ['test']); + + jest.spyOn(paginationService, 'filterIn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterDate', () => { + it('should be return a value', async () => { + const result: Record = paginationService.filterDate( + 'name', + new Date() + ); + + jest.spyOn(paginationService, 'filterDate').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/setting/setting.service.spec.ts b/test/unit/setting/setting.service.spec.ts new file mode 100644 index 0000000..6c0a581 --- /dev/null +++ b/test/unit/setting/setting.service.spec.ts @@ -0,0 +1,585 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingModule } from 'src/common/setting/setting.module'; +import configs from 'src/configs'; + +describe('SettingService', () => { + let settingService: SettingService; + let setting: SettingDoc; + const settingName1 = `${faker.name.jobArea()}${+new Date()}`; + const settingName2 = `${faker.name.jobArea()}${+new Date()}`; + const settingName3 = `${faker.name.jobArea()}${+new Date()}`; + const settingName4 = `${faker.name.jobArea()}${+new Date()}`; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + SettingModule, + ], + }).compile(); + + settingService = moduleRef.get(SettingService); + + setting = await settingService.create({ + name: `${faker.name.jobArea()}${+new Date()}`, + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + value: 'true', + }); + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ _id: setting._id }); + await settingService.deleteMany({ + name: { + $in: [ + settingName1, + settingName2, + settingName3, + settingName4, + ], + }, + }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(settingService).toBeDefined(); + }); + + describe('findAll', () => { + it('get all setting', async () => { + const result: SettingEntity[] = await settingService.findAll({ + name: setting.name, + }); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + + it('get all setting with limit and offset', async () => { + const result = await settingService.findAll( + { + name: setting.name, + }, + { paging: { limit: 1, offset: 0 } } + ); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + + it('get all setting with limit, offset, and sort', async () => { + const result = await settingService.findAll( + { + name: setting.name, + }, + { + paging: { limit: 1, offset: 0 }, + order: { name: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC }, + } + ); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + }); + + describe('getTotal', () => { + it('should return a number of total data', async () => { + const result: number = await settingService.getTotal({ + name: setting.name, + }); + + jest.spyOn(settingService, 'getTotal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + }); + + describe('findOneById', () => { + it('should be success', async () => { + const result: SettingDoc = await settingService.findOneById( + setting._id + ); + + jest.spyOn(settingService, 'findOneById').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('findOneByName', () => { + it('should be return a setting entity', async () => { + const result: SettingDoc = await settingService.findOneByName( + setting.name + ); + + jest.spyOn(settingService, 'findOneByName').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('create', () => { + it('should be create a new setting, number', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName1, + type: ENUM_SETTING_DATA_TYPE.NUMBER, + description: 'aaa', + value: '1', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName1); + }); + + it('should be create a new setting, string', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName2, + description: 'test', + type: ENUM_SETTING_DATA_TYPE.STRING, + value: '1', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName2); + }); + + it('should be create a new setting, boolean', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName3, + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + description: 'aaa', + value: 'true', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName3); + }); + + it('should be create a new setting, string', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName4, + description: 'test', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + value: '1,2,3', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName4); + }); + }); + + describe('updateValue', () => { + it('should be update a value, number', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: '1', + type: ENUM_SETTING_DATA_TYPE.NUMBER, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.NUMBER); + expect(result.value).toBe('1'); + }); + + it('should be update a value, string', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'aaa', + type: ENUM_SETTING_DATA_TYPE.STRING, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.STRING); + expect(result.value).toBe('aaa'); + }); + + it('should be update a value, boolean', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.BOOLEAN); + expect(result.value).toBe('true'); + }); + + it('should be update a value, array of string', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'aa,bb,cc', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING); + expect(result.value).toBe('aa,bb,cc'); + }); + }); + + describe('delete', () => { + it('should be success', async () => { + const result: SettingDoc = await settingService.delete(setting); + + jest.spyOn(settingService, 'delete').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('getMaintenance', () => { + it('should be return a setting', async () => { + const result: boolean = await settingService.getMaintenance(); + + jest.spyOn(settingService, 'getMaintenance').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + }); + }); + + describe('getMobileNumberCountryCodeAllowed', () => { + it('should be return a setting', async () => { + const result: string[] = + await settingService.getMobileNumberCountryCodeAllowed(); + + jest.spyOn( + settingService, + 'getMobileNumberCountryCodeAllowed' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('getPasswordAttempt', () => { + it('should be return a setting', async () => { + const result: boolean = await settingService.getPasswordAttempt(); + + jest.spyOn( + settingService, + 'getPasswordAttempt' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('getMaxPasswordAttempt', () => { + it('should be return a setting', async () => { + const result: number = await settingService.getMaxPasswordAttempt(); + + jest.spyOn( + settingService, + 'getMaxPasswordAttempt' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('deleteMany', () => { + it('should be success', async () => { + const result: boolean = await settingService.deleteMany({ + _id: setting._id, + }); + jest.spyOn(settingService, 'deleteMany').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('getValue', () => { + it('should be return a number value', async () => { + const setting1: SettingDoc = await settingService.create({ + name: settingName1, + value: '1', + type: ENUM_SETTING_DATA_TYPE.NUMBER, + }); + const result: number = await settingService.getValue( + setting1 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('number'); + }); + + it('should be return a string value', async () => { + const setting2: SettingDoc = await settingService.create({ + name: settingName2, + value: 'aaa', + type: ENUM_SETTING_DATA_TYPE.STRING, + }); + const result: string = await settingService.getValue( + setting2 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('should be return a boolean value true', async () => { + const setting3: SettingDoc = await settingService.create({ + name: settingName3, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + const result: boolean = await settingService.getValue( + setting3 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('boolean'); + }); + + it('should be return a boolean value false', async () => { + const setting3: SettingDoc = await settingService.create({ + name: settingName3, + value: 'false', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + const result: boolean = await settingService.getValue( + setting3 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(typeof result).toBe('boolean'); + }); + + it('should be return a array of string value', async () => { + const setting4: SettingDoc = await settingService.create({ + name: settingName4, + value: '1,2,3', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + }); + const result: string[] = await settingService.getValue( + setting4 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(typeof result[0]).toBe('string'); + }); + }); + + describe('checkValue', () => { + it('should be check a number value', async () => { + const result: boolean = await settingService.checkValue( + '1', + ENUM_SETTING_DATA_TYPE.NUMBER + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a string value', async () => { + const result: boolean = await settingService.checkValue( + 'aaaa', + ENUM_SETTING_DATA_TYPE.STRING + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a boolean true value', async () => { + const result: boolean = await settingService.checkValue( + 'true', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a boolean false value', async () => { + const result: boolean = await settingService.checkValue( + 'false', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a array of string value', async () => { + const result: boolean = await settingService.checkValue( + '1,2,3', + ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check error', async () => { + const result: boolean = await settingService.checkValue( + 'trueaaa', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..359a2f4 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "dist", + "*coverage", + "logs", + "scripts", + "test" + ], + "include": [ + "src" + ], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e0984c3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": false, + "target": "ESNext", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": [ + "src", + "test", + ], + "exclude": [ + "node_modules", + "dist", + "*coverage", + "logs", + "scripts", + ] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..c3126da --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8140 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@angular-devkit/core@14.0.5": + version "14.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.0.5.tgz#19f5940b53aeb0ce56479c44670d3bc3b2df92b1" + integrity sha512-/CUGi6QLwh79FvsOY7M+1LQL3asZsbQW/WBd5f1iu5y7TLNqCwo+wOb0ZXLDNPw45vYBxFajtt3ob3U7qx3jNg== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.0.0" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/core@15.0.4": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.0.4.tgz#257ba1d76cd106216d0150f480d0062e726af996" + integrity sha512-4ITpRAevd652SxB+qNesIQ9qfbm7wT5UBU5kJOPPwGL77I21g8CQpkmV1n5VSacPvC9Zbz90feOWexf7w7JzcA== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.2.0" + rxjs "6.6.7" + source-map "0.7.4" + +"@angular-devkit/core@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.1.4.tgz#462f123d56f9298cb04b3fa31b425fc31abb76c5" + integrity sha512-PW5MRmd9DHJR4FaXchwQtj9pXnsghSTnwRvfZeCRNYgU2sv0DKyTV+YTSJB+kNXnoPNG1Je6amDEkiXecpspXg== + dependencies: + ajv "8.12.0" + ajv-formats "2.1.1" + jsonc-parser "3.2.0" + rxjs "6.6.7" + source-map "0.7.4" + +"@angular-devkit/schematics-cli@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-15.1.4.tgz#f2ea0379e27ddd6b05b302dd88b8d3f1b6c49ec8" + integrity sha512-qkM5Mfs28jZzNcJnSM6RlyrKkYvzhQmWFTxBXnn15k5T4EnSs1gI6O054Xn7jo/senfwNNt7h2Mlz2OmBLo6+w== + dependencies: + "@angular-devkit/core" "15.1.4" + "@angular-devkit/schematics" "15.1.4" + ansi-colors "4.1.3" + inquirer "8.2.4" + symbol-observable "4.0.0" + yargs-parser "21.1.1" + +"@angular-devkit/schematics@14.0.5": + version "14.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.0.5.tgz#01777d2ad473d35bdfdbbb751521c43421ad9772" + integrity sha512-sufxITBkn2MvgEREt9JQ3QCKHS+sue1WsVzLE+TWqG5MC/RPk0f9tQ5VoHk6ZTzDKUvOtSoc7G+n0RscQsyp5g== + dependencies: + "@angular-devkit/core" "14.0.5" + jsonc-parser "3.0.0" + magic-string "0.26.1" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@15.0.4": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.0.4.tgz#64de42f9100d7080bc3c59bb06d1e4f6f15a088e" + integrity sha512-/gXiLFS0+xFdx6wPoBpe/c6/K9I5edMpaASqPf4XheKtrsSvL+qTlIi3nsbfItzOiDXbaBmlbxGfkMHz/yg0Ig== + dependencies: + "@angular-devkit/core" "15.0.4" + jsonc-parser "3.2.0" + magic-string "0.26.7" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.1.4.tgz#30e38777f1bd98e20e6dbe1bfddabc3bcd42605f" + integrity sha512-jpddxo9Qd2yRQ1t9FLhAx5S+luz6HkyhDytq0LFKbxf9ikf1J4oy9riPBFl4pRmrNARWcHZ6GbD20/Ky8PjmXQ== + dependencies: + "@angular-devkit/core" "15.1.4" + jsonc-parser "3.2.0" + magic-string "0.27.0" + ora "5.4.1" + rxjs "6.6.7" + +"@aws-crypto/crc32@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" + integrity sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/crc32c@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz#016c92da559ef638a84a245eecb75c3e97cb664f" + integrity sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz#640ae66b4ec3395cee6a8e94ebcd9f80c24cd688" + integrity sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha1-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz#f9083c00782b24714f528b1a1fef2174002266a3" + integrity sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz#05f160138ab893f1c6ba5be57cfd108f05827766" + integrity sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/sha256-js" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@3.0.0", "@aws-crypto/sha256-js@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz#f06b84d550d25521e60d2a0e2a90139341e007c2" + integrity sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz#5d1bf825afa8072af2717c3e455f35cda0103ec2" + integrity sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/util@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" + integrity sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w== + dependencies: + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/abort-controller@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.292.0.tgz#37c43fd2ce5bcb158aa62e3a5632045ee8a7e3cc" + integrity sha512-lf+OPptL01kvryIJy7+dvFux5KbJ6OTwLPPEekVKZ2AfEvwcVtOZWFUhyw3PJCBTVncjKB1Kjl3V/eTS3YuPXQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader-native@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.292.0.tgz#143fcedfe0bc583bd089dee0d6247b22f5e0db4d" + integrity sha512-A34sBrnggm9mXPZeeEie4jDv9zHRMS0LSm85VkfrBLuYYsfsw9DxmW59wJkuo6DIm/RK04oH5+lRMt34koBgrw== + dependencies: + "@aws-sdk/util-base64" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.292.0.tgz#f3a661cf15c8bacbbc761cca8c8eb13625543eb3" + integrity sha512-ccFPnzBjLbDCmFjTXwhsfD58vtEiAjbor3A9tvnou+3Dj6RrMEGPaTu5tcw3mwWb2zh1K3HFJg6Bmb0no49TRw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/client-s3@^3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.292.0.tgz#13c63a34992c48e8d5145624a80c97f90ba81655" + integrity sha512-avqj7YCicFdB/jZXvbMhe9b0y/GIdJpIGgxZV/RowuEqVan1rlKoHEKnxmTwt/CPz02byLOPIXQ55yDVP7/FvQ== + dependencies: + "@aws-crypto/sha1-browser" "3.0.0" + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.292.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-node" "3.292.0" + "@aws-sdk/eventstream-serde-browser" "3.292.0" + "@aws-sdk/eventstream-serde-config-resolver" "3.292.0" + "@aws-sdk/eventstream-serde-node" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-blob-browser" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/hash-stream-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/md5-js" "3.292.0" + "@aws-sdk/middleware-bucket-endpoint" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-expect-continue" "3.292.0" + "@aws-sdk/middleware-flexible-checksums" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-location-constraint" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-sdk-s3" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/middleware-ssec" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4-multi-region" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-stream-browser" "3.292.0" + "@aws-sdk/util-stream-node" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + "@aws-sdk/util-waiter" "3.292.0" + "@aws-sdk/xml-builder" "3.292.0" + fast-xml-parser "4.1.2" + tslib "^2.3.1" + +"@aws-sdk/client-sso-oidc@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.292.0.tgz#8354f5a9e672dc705e769d489731886d52552b1c" + integrity sha512-KANoinZDvWwCXKrx92V0i8ItovKwW94Ep4vLY+D7ZmuV8IACK0XcIR9HF8eMR4Zqy7DSBAGdvvd318Qy2v1f2Q== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/client-sso@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.292.0.tgz#977a6834deb946571423c1af8f306157bfee1903" + integrity sha512-DzBBa72TTgfTllvTbD/7KcRY8bo5ExUv8gHJaedrE7mlZUn/2msk9S41rf+Rcwb0bf7k14Y36aRVwoXwQCKPLg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/client-sts@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.292.0.tgz#7d0c66f761c6d1dec9bd67ed5ae04320753a56dd" + integrity sha512-t9Q0+iT8E1QAARq7aUHdF5KwgXrdW1yl4lsnkmVcLJKypyhnXTVJ68qldV6rBDSFswGqT0SBQBzcAj6vPNlOFQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-node" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-sdk-sts" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + fast-xml-parser "4.1.2" + tslib "^2.3.1" + +"@aws-sdk/config-resolver@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.292.0.tgz#c5c9b86a2a75aa591bc7acdbe94557367a2a7d90" + integrity sha512-cB3twnNR7vYvlt2jvw8VlA1+iv/tVzl+/S39MKqw2tepU+AbJAM0EHwb/dkf1OKSmlrnANXhshx80MHF9zL4mA== + dependencies: + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-env@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.292.0.tgz#bde3333b7bee715c8a41113f1c6deb0e896a59da" + integrity sha512-YbafSG0ZEKE2969CJWVtUhh3hfOeLPecFVoXOtegCyAJgY5Ghtu4TsVhL4DgiGAgOC30ojAmUVQEXzd7xJF5xA== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-imds@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.292.0.tgz#557e59c637c3852cac54534319c75eb015aa3081" + integrity sha512-W/peOgDSRYulgzFpUhvgi1pCm6piBz6xrVN17N4QOy+3NHBXRVMVzYk6ct2qpLPgJUSEZkcpP+Gds+bBm8ed1A== + dependencies: + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-ini@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.292.0.tgz#1d67ea9f560d084051d63f5695a1ddcede09e0ad" + integrity sha512-gTXSGjx3Q+KY8Zz/XHTDWOBx9UWtL3s8tTdpQOdaMqqm0xIK5X4KDud3L/huPpZYm0a7rNAML8l1mU56FFnBVw== + dependencies: + "@aws-sdk/credential-provider-env" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/credential-provider-process" "3.292.0" + "@aws-sdk/credential-provider-sso" "3.292.0" + "@aws-sdk/credential-provider-web-identity" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.292.0.tgz#c2620b28f9f04c01e66378539075f23a4ff6862d" + integrity sha512-85LQIeSGSQtbrgqEYmCcUnehBmTKt8bbn7mN9RxbtCDnZVgEagJCid7o9+fYQXZ5IjXaHLUApoLsv6ytEj4ITA== + dependencies: + "@aws-sdk/credential-provider-env" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/credential-provider-ini" "3.292.0" + "@aws-sdk/credential-provider-process" "3.292.0" + "@aws-sdk/credential-provider-sso" "3.292.0" + "@aws-sdk/credential-provider-web-identity" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-process@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.292.0.tgz#52caa9d46d227e02fda5807d32a177a0819eee97" + integrity sha512-CFVXuMuUvg/a4tknzRikEDwZBnKlHs1LZCpTXIGjBdUTdosoi4WNzDLzGp93ZRTtcgFz+4wirz2f7P3lC0NrQw== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-sso@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.292.0.tgz#ae01ab2ff4771ebf5302f264200a116a6b9cdc00" + integrity sha512-+jrhi0oZc9dMtbRsqi+lkqIheCb8QlsRJSEKDa3nUlyxaOkzRKR9Yf5Jtpqooa0ichFhMVZTD9oXPFrlGROIEQ== + dependencies: + "@aws-sdk/client-sso" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/token-providers" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-web-identity@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.292.0.tgz#60e180eadd0947891ed041f6a4574fa2074d0d4c" + integrity sha512-4DbtIEM9gGVfqYlMdYXg3XY+vBhemjB1zXIequottW8loLYM8Vuz4/uGxxKNze6evVVzowsA0wKrYclE1aj/Rg== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-codec@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.292.0.tgz#6c4f34a0f7bf9113bc26f3c736fb0c023cb87e43" + integrity sha512-P0np4vhCKf/JH6I39Id8DxZR+UZzG+Br+vOrTinerMfOhzTa2229XmL8pwlMpOoxnJLMPmEDtD1KQqLslBEXtw== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.292.0.tgz#090af8854387056e9674b8688a815b93c8548fd6" + integrity sha512-VzRbJqqE444GOuoNTxTJ1dC1IhNhA6jfHjgsI8iDRHraaEukGqsPx1vkc+byxrDEjgxKN5IqOwZ4yJWMIAozBA== + dependencies: + "@aws-sdk/eventstream-serde-universal" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-config-resolver@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.292.0.tgz#79b1b2194a0386dae6e4134c1534e5ce62a18312" + integrity sha512-Ndx+qJyWmBCW9FSm68AGLoO4AZ0AaL/wjpJEgFF2sZBWjYe9O9PB9IGR/yuqCBTElf3YtSiFMsloikQaz2ft6g== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.292.0.tgz#2001ab936ce316aa713d69beecc133a77300d445" + integrity sha512-NFCEiNCetNye7jQfRd5y/7J9dLg9+uL57698wYeXeadlwJ8Cd/Nhsz+t7RIbP05VqshU+anXARMB1avl9oAijQ== + dependencies: + "@aws-sdk/eventstream-serde-universal" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-universal@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.292.0.tgz#841b0488ce526f40a7f4c69b77af5ea0b7669f12" + integrity sha512-1gqZNx+S1EUpl3Tq6uIesiDx8gnkpXqPsFfCZT7lSWWXBpnHmnUZAh3jbiO9UlQbYuB9SfT0EBKb1iOY9z4j1Q== + dependencies: + "@aws-sdk/eventstream-codec" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/fetch-http-handler@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.292.0.tgz#a99d915e019e888bfdfa3e5da68606bfc4c80522" + integrity sha512-zh3bhUJbL8RSa39ZKDcy+AghtUkIP8LwcNlwRIoxMQh3Row4D1s4fCq0KZCx98NJBEXoiTLyTQlZxxI//BOb1Q== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/querystring-builder" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-blob-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.292.0.tgz#d62d8556877f0823fdcc3f08dac634389219e62e" + integrity sha512-4+Fm4IOkxGqgx8dU0EbExCq6xx30y369ZSXz89h9YDQYdJ2Muw7iNCHAg/4VM+gfp0vo9J8zPOTsSju8LNS5Jg== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.292.0" + "@aws-sdk/chunked-blob-reader-native" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.292.0.tgz#4f62e36a7cdefd0f4bca4c1d16261d36a4596442" + integrity sha512-1yLxmIsvE+eK36JXEgEIouTITdykQLVhsA5Oai//Lar6Ddgu1sFpLDbdkMtKbrh4I0jLN9RacNCkeVQjZPTCCQ== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-buffer-from" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-stream-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.292.0.tgz#1a5c17c322bd02f5a14046a10a664c4d96cd06de" + integrity sha512-p2nj9A5lZKQU45Q4Od3iZDvpziEpojAyuyAI0HPzpIuJIfzFQ0/7pMBKde1li6wq93rpyFLwNufV6FEZnKCYRg== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/invalid-dependency@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.292.0.tgz#0e5b47cacf459db6ae8dddc02d613a5bd0ff3555" + integrity sha512-39OUV78CD3TmEbjhpt+V+Fk4wAGWhixqHxDSN8+4WL0uB4Fl7k5m3Z9hNY78AttHQSl2twR7WtLztnXPAFsriw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/is-array-buffer@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.292.0.tgz#d599c7ad4ad104918d52b8d2160091ca5b0a1971" + integrity sha512-kW/G5T/fzI0sJH5foZG6XJiNCevXqKLxV50qIT4B1pMuw7regd4ALIy0HwSqj1nnn9mSbRWBfmby0jWCJsMcwg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/md5-js@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.292.0.tgz#740b4e6bbe24a41fefb78f93f436da55e438555d" + integrity sha512-ngfsKLgQenXW3EbsDf47PVNys1SecTbsq6k88h7+Aa8BU49+9ZOIz4VDpWuPiNyYpeV7jJdl1dfD+ujOYvvgNw== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-bucket-endpoint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.292.0.tgz#28f7bfc3dfeef43598dee96aa597cadee060a117" + integrity sha512-XRy9RSUIRcbxYfH504ywhQllgfdf3wVhk2k0mMPYnUbeEhAFe1/eUog2v/bi07/q5TQ4Hppi+W3nHCVualQEow== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-content-length@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.292.0.tgz#f2035aee536abf553b743202879ee86171c4c3c7" + integrity sha512-2gMWzQus5mj14menolpPDbYBeaOYcj7KNFZOjTjjI3iQ0KqyetG6XasirNrcJ/8QX1BRmpTol8Xjp2Ue3Gbzwg== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-endpoint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.292.0.tgz#c6809a2e001ab03cac223dfae48439e893da627b" + integrity sha512-cPMkiSxpZGG6tYlW4OS+ucS6r43f9ddX9kcUoemJCY10MOuogdPjulCAjE0HTs2PLKSOrrG4CTP4Q4wWDrH4Bw== + dependencies: + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-expect-continue@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.292.0.tgz#11edbf77d741ab169a469c918bb4b39dd9604438" + integrity sha512-bZ2bsBud3E6BebZWGxVcWxBSg09bP0KyX8PT0jI66JM0yTbZSJhoGhlKAqfNG46R9h4K5tCYB2uYgV/3oU/ZpQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-flexible-checksums@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.292.0.tgz#f9a52631d65ead667457c57bcf435c2fcee72d13" + integrity sha512-AxU/Gb+TRdl/0jHmbreYh3QnB0jR25zgjPZ4/JbGBJ2SQI9jm3LCNK9XOrPUmZp/vu9wsvyxtmKQidpQ5+FX5w== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-crypto/crc32c" "3.0.0" + "@aws-sdk/is-array-buffer" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-host-header@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.292.0.tgz#513b011fcabedf29e0a6706a4aa3867bc7d813e4" + integrity sha512-mHuCWe3Yg2S5YZ7mB7sKU6C97XspfqrimWjMW9pfV2usAvLA3R0HrB03jpR5vpZ3P4q7HB6wK3S6CjYMGGRNag== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-location-constraint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.292.0.tgz#3b138685854cfbf48abd70c40cd56a09a74cab9a" + integrity sha512-WTbMyoCckdkmq7Yok0gI4226gTmxP/zM1fbFiC+liZXBJ+H5EvIFmu30tWbX+4m41LL/XQVm65olXJFwhoExGQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-logger@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.292.0.tgz#dd5ca0f20b06b1b74f918ddf0264ece1e9887aa1" + integrity sha512-yZNY1XYmG3NG+uonET7jzKXNiwu61xm/ZZ6i/l51SusuaYN+qQtTAhOFsieQqTehF9kP4FzbsWgPDwD8ZZX9lw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-recursion-detection@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.292.0.tgz#d422bbc9efa2df2481ad56d0db553b0c0652e615" + integrity sha512-kA3VZpPko0Zqd7CYPTKAxhjEv0HJqFu2054L04dde1JLr43ro+2MTdX7vsHzeAFUVRphqatFFofCumvXmU6Mig== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-retry@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.292.0.tgz#7511f06c6e5b1b65d572ce2f596728209a2159cd" + integrity sha512-wUuXwiwMwFNMTgc9oFeUHkgpF56EfLJl/EtRn2376k9sFd7JoFu3zTo3VTGROLH/88r20A01TOr9g/cFjXgCJQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/service-error-classification" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/middleware-sdk-s3@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.292.0.tgz#ba921fd85417d7bc16423669234c4dbb42b0982e" + integrity sha512-kEUmh3ZM34H+2bEQfpZhVotJCNYpSbq9Q4YxlWVbnjiO/VS+S9BFEM3Fcj5+EzEgI02tNNi6/qTXj3iS8tT6hA== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-sdk-sts@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.292.0.tgz#927cecb0167b84aceddc959039f368ea2a593e87" + integrity sha512-GN5ZHEqXZqDi+HkVbaXRX9HaW/vA5rikYpWKYsmxTUZ7fB7ijvEO3co3lleJv2C+iGYRtUIHC4wYNB5xgoTCxg== + dependencies: + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-serde@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.292.0.tgz#4834ee9b03c50e11349306753c27086bac4dac08" + integrity sha512-6hN9mTQwSvV8EcGvtXbS/MpK7WMCokUku5Wu7X24UwCNMVkoRHLIkYcxHcvBTwttuOU0d8hph1/lIX4dkLwkQw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-signing@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.292.0.tgz#51868199d23d28d264a06adcec52373c8da88c85" + integrity sha512-GVfoSjDjEQ4TaO6x9MffyP3uRV+2KcS5FtexLCYOM9pJcnE9tqq9FJOrZ1xl1g+YjUVKxo4x8lu3tpEtIb17qg== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-ssec@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.292.0.tgz#c42a7b6a9b0e1a197d9fd6a9f497f10f785fcfd0" + integrity sha512-VfwrTEs9nYU6sCnt/cffhnJ2djGkMyMbBEysMZm2HEbFMloGKBd0Wtvk9y+SWPa6+DDRe2CqqX8jMzrO4JT4Eg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-stack@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.292.0.tgz#279f4b688d91f9757cedd5311ae86ad6e3e6ac63" + integrity sha512-WdQpRkuMysrEwrkByCM1qCn2PPpFGGQ2iXqaFha5RzCdZDlxJni9cVNb6HzWUcgjLEYVTXCmOR9Wxm3CNW44Qg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/middleware-user-agent@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.292.0.tgz#439014fa5de7f2113110f28c1e8ef76db7ee0210" + integrity sha512-PvGMfPwfW1nq9fzWKIIS6USjY70FfdmiZhFL/TyoaTp8gV/Y1+Le8i6E1LegDbnbE/LS5IBuNgUzdserYcfbOQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/node-config-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.292.0.tgz#52817db9e056fedb967704b156fde4b5516dacf1" + integrity sha512-S3NnC9dQ5GIbJYSDIldZb4zdpCOEua1tM7bjYL3VS5uqCEM93kIi/o/UkIUveMp/eqTS2LJa5HjNIz5Te6je0A== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/node-http-handler@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.292.0.tgz#f7a8fca359932ba56acf65eafd169db9d2cebc9d" + integrity sha512-L/E3UDSwXLXjt1XWWh0RBD55F+aZI1AEdPwdES9i1PjnZLyuxuDhEDptVibNN56+I9/4Q3SbmuVRVlOD0uzBag== + dependencies: + "@aws-sdk/abort-controller" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/querystring-builder" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/property-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.292.0.tgz#2bdf9f6e15521350936636107a2057a19c1e55ec" + integrity sha512-dHArSvsiqhno/g55N815gXmAMrmN8DP7OeFNqJ4wJG42xsF2PFN3DAsjIuHuXMwu+7A3R1LHqIpvv0hA9KeoJQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/protocol-http@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.292.0.tgz#1829036bdec59698f44daadb590e3fa552494955" + integrity sha512-NLi4fq3k41aXIh1I97yX0JTy+3p6aW1NdwFwdMa674z86QNfb4SfRQRZBQe9wEnAZ/eWHVnlKIuII+U1URk/Kg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-builder@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.292.0.tgz#a2fd9c2540a80718fb2f52c606926f8d2e08a695" + integrity sha512-XElIFJaReIm24eEvBtV2dOtZvcm3gXsGu/ftG8MLJKbKXFKpAP1q+K6En0Bs7/T88voKghKdKpKT+eZUWgTqlg== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-uri-escape" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.292.0.tgz#32645c834b4dd1660176bf0b6df201d688242c66" + integrity sha512-iTYpYo7a8X9RxiPbjjewIpm6XQPx2EOcF1dWCPRII9EFlmZ4bwnX+PDI36fIo9oVs8TIKXmwNGODU9nsg7CSAw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/service-error-classification@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.292.0.tgz#8fef4ee8e553218234eca91dd479902092b12bac" + integrity sha512-X1k3sixCeC45XSNHBe+kRBQBwPDyTFtFITb8O5Qw4dS9XWGhrUJT4CX0qE5aj8qP3F9U5nRizs9c2mBVVP0Caw== + +"@aws-sdk/shared-ini-file-loader@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.292.0.tgz#08260536116c4e0b44ebd0d0bd197ff15815090f" + integrity sha512-Av2TTYg1Jig2kbkD56ybiqZJB6vVrYjv1W5UQwY/q3nA/T2mcrgQ20ByCOt5Bv9VvY7FSgC+znj+L4a7RLGmBg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4-multi-region@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.292.0.tgz#01734775497474476d84733114b3eb129a48759e" + integrity sha512-MjWEIjbAr7n9vsFeLpoRzNSYFgWOROf1mLj6Db8TfRowaortUBO7PbleLV4n3SPujSnxhaVBzlmnCY2AjatH9g== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.292.0.tgz#1fbb9ceea4c80c079b64f836af365985970f2a5f" + integrity sha512-+rw47VY5mvBecn13tDQTl1ipGWg5tE63faWgmZe68HoBL87ZiDzsd7bUKOvjfW21iMgWlwAppkaNNQayYRb2zg== + dependencies: + "@aws-sdk/is-array-buffer" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + "@aws-sdk/util-uri-escape" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/smithy-client@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.292.0.tgz#232b7bac2115d52390057bab6a79d14cffe06698" + integrity sha512-S8PKzjPkZ6SXYZuZiU787dMsvQ0d/LFEhw2OI4Oe2An9Fc2IwJ2FYukyHoQJOV2tV0DiuMebPo7eMyQyjKElvA== + dependencies: + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/token-providers@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.292.0.tgz#77fd49bbd04ac52ea27e41091478491de04a60f9" + integrity sha512-RJ+fQp/SsMnuH+WrTWaLR2Kq1b/fQdSq4zDwtauultSEBQknd7RAgjQ4JBVaIwR66vJjQPa3MXYfgja/oONT+w== + dependencies: + "@aws-sdk/client-sso-oidc" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/types@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.292.0.tgz#54aa7347123116ac368f08df5e02954207328c63" + integrity sha512-1teYAY2M73UXZxMAxqZxVS2qwXjQh0OWtt7qyLfha0TtIk/fZ1hRwFgxbDCHUFcdNBSOSbKH/ESor90KROXLCQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/types@^3.222.0": + version "3.257.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.257.0.tgz#4951ee3456cd9a46829516f5596c2b8a05ffe06a" + integrity sha512-LmqXuBQBGeaGi/3Rp7XiEX1B5IPO2UUfBVvu0wwGqVsmstT0SbOVDZGPmxygACbm64n+PRx3uTSDefRfoiWYZg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/url-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.292.0.tgz#b8b81d1c099e248813afbc33206e24b97f14228a" + integrity sha512-NZeAuZCk1x6TIiWuRfbOU6wHPBhf0ly2qOHzWut4BCH+b4RrDmFF8EmXcH1auEfGhE7yRyR6XqIN0t3S+hYACA== + dependencies: + "@aws-sdk/querystring-parser" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-arn-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.292.0.tgz#079e866585ebb19aeb50fe01712e384ef90e80b0" + integrity sha512-xfE4U94TfjMC2WNNDte/kDByf16GrQKaS0BKsm+Fk/PaeHUofEp8suOEz/EVdEoa3Ayy2Uc5QdhrGnlqf8MxeA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64/-/util-base64-3.292.0.tgz#b07fc9752edad18b32ad4b1cc752b5df2d133377" + integrity sha512-zjNCwNdy617yFvEjZorepNWXB2sQCVfsShCwFy/kIQ5iW5tT2jQKaqc0K77diU9atkooxw9p1W9m9sOgrkOFNw== + dependencies: + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-body-length-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.292.0.tgz#1baefd126c8881ff140c83111aeb79c6d5b21cb3" + integrity sha512-Wd/BM+JsMiKvKs/bN3z6TredVEHh2pKudGfg3CSjTRpqFpOG903KDfyHBD42yg5PuCHoHoewJvTPKwgn7/vhaw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-body-length-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.292.0.tgz#9f3f91c80e9b4e2afb226550e9a0b3acde8bcd02" + integrity sha512-BBgipZ2P6RhogWE/qj0oqpdlyd3iSBYmb+aD/TBXwB2lA/X8A99GxweBd/kp06AmcJRoMS9WIXgbWkiiBlRlSA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-buffer-from@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.292.0.tgz#b2d0eff4e63b0cc8a5d5dc133b76c3fe3daee2fc" + integrity sha512-RxNZjLoXNxHconH9TYsk5RaEBjSgTtozHeyIdacaHPj5vlQKi4hgL2hIfKeeNiAfQEVjaUFF29lv81xpNMzVMQ== + dependencies: + "@aws-sdk/is-array-buffer" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-config-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.292.0.tgz#6a9c7b7e29028135862ba880c615e2f975d68c6d" + integrity sha512-t3noYll6bPRSxeeNNEkC5czVjAiTPcsq00OwfJ2xyUqmquhLEfLwoJKmrT1uP7DjIEXdUtfoIQ2jWiIVm/oO5A== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.292.0.tgz#8890ee4ff8939c9ada363cae14ec7196269ff14c" + integrity sha512-7+zVUlMGfa8/KT++9humHo6IDxTnxMCmWUj5jVNlkpk6h7Ecmppf7aXotviyVIA43lhtz0p2AErs0N0ekEUK+w== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.292.0.tgz#fc7f54cd935b8974d1b16d6c8bed8b9ae99af20e" + integrity sha512-SSIw85eF4BVs0fOJRyshT+R3b/UmBPhiVKCUZm2rq6+lIGkDPiSwQU3d/80AhXtiL5SFT/IzAKKgQd8qMa7q3A== + dependencies: + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-endpoints@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.292.0.tgz#cb6d8259efc4b3f73da0b326ef38495d9bbbf04f" + integrity sha512-CvNES1YaickVE8Iu2EP4ywdiCNy8thRnyXdx7v1d39NLeTQuMWJyM/cazWQIBv0WPYOrAnjsWb5Nw05GwpwSdA== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-hex-encoding@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.292.0.tgz#a8b8b989fcf518a18606cb6d81f90d92b0660db4" + integrity sha512-qBd5KFIUywQ3qSSbj814S2srk0vfv8A6QMI+Obs1y2LHZFdQN5zViptI4UhXhKOHe+NnrHWxSuLC/LMH6q3SmA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz#a4136a20ee1bfcb73967a6614caf769ef79db070" + integrity sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-middleware@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.292.0.tgz#d4819246c66229df405850004d9e3ae4a6fca8ea" + integrity sha512-KjhS7flfoBKDxbiBZjLjMvEizXgjfQb7GQEItgzGoI9rfGCmZtvqCcqQQoIlxb8bIzGRggAUHtBGWnlLbpb+GQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-retry@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.292.0.tgz#a72dd74760864aa03feb00f2cee8b97c25c297c4" + integrity sha512-JEHyF7MpVeRF5uR4LDYgpOKcFpOPiAj8TqN46SVOQQcL1K+V7cSr7O7N7J6MwJaN9XOzAcBadeIupMm7/BFbgw== + dependencies: + "@aws-sdk/service-error-classification" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-browser/-/util-stream-browser-3.292.0.tgz#6476fef12bea839ef25b80c79817f3932ec019d0" + integrity sha512-yzwpjq18oefyp/Sv+Z0VWh7ziRPp+qM0pDUrTfuAnXg+mrlxaPDXJOhp5LoY8AVHcDPOEdIbzz0b00G48FabIg== + dependencies: + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-node/-/util-stream-node-3.292.0.tgz#4a83004729cdc4a8fb03c96921f59939ffc31d3b" + integrity sha512-p3DHXvWo4Zdka75HwewUnWjpFp/gOT4SYYEOAsv3BwuZGxfmnojK9OVCkUBJ7s6LeHMKTgGqQPwAnVFu7iIZNg== + dependencies: + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-uri-escape@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.292.0.tgz#306a36e3574af3509c542c7224669082f6abc633" + integrity sha512-hOQtUMQ4VcQ9iwKz50AoCp1XBD5gJ9nly/gJZccAM7zSA5mOO8RRKkbdonqquVHxrO0CnYgiFeCh3V35GFecUw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.292.0.tgz#26c4e5ffbe046cebe9d15c357839ea38ada95c56" + integrity sha512-dld+lpC3QdmTQHdBWJ0WFDkXDSrJgfz03q6mQ8+7H+BC12ZhT0I0g9iuvUjolqy7QR00OxOy47Y9FVhq8EC0Gg== + dependencies: + "@aws-sdk/types" "3.292.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.292.0.tgz#9065307641eb246f32fee78eec5d961cffbba6a9" + integrity sha512-f+NfIMal5E61MDc5WGhUEoicr7b1eNNhA+GgVdSB/Hg5fYhEZvFK9RZizH5rrtsLjjgcr9nPYSR7/nDKCJLumw== + dependencies: + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-utf8-browser@^3.0.0": + version "3.109.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.109.0.tgz#d013272e1981b23a4c84ac06f154db686c0cf84e" + integrity sha512-FmcGSz0v7Bqpl1SE8G1Gc0CtDpug+rvqNCG/szn86JApD/f5x8oByjbEiAyTU2ZH2VevUntx6EW68ulHyH+x+w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-utf8@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8/-/util-utf8-3.292.0.tgz#c12049a01de36f1133232f95cbb0c0177e8d3c36" + integrity sha512-FPkj+Z59/DQWvoVu2wFaRncc3KVwe/pgK3MfVb0Lx+Ibey5KUx+sNpJmYcVYHUAe/Nv/JeIpOtYuC96IXOnI6w== + dependencies: + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-waiter@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.292.0.tgz#860b6615f1d5d0cd545b2d5fefd0bb3c03b0a32d" + integrity sha512-+7j+mcWUY4GwU8nTK4MvLWpOzS34SJZL85qLxQ04pysoCSHkInyS51D1ejBVNlJdbUSFvIcU0WHU0y6MDDeJzg== + dependencies: + "@aws-sdk/abort-controller" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/xml-builder@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.292.0.tgz#1c53b6b4eac1b841bd5a4581289791c46cd39743" + integrity sha512-0zgnhdwUy30q/1NPXi5ekdzHQqCs3ZJaUeGbvYMO54osi4K5hygAyTsyWtv6oaJggRqZrB0LAZ9xN6hG+sA8/g== + dependencies: + tslib "^2.3.1" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.6.tgz#8b37d24e88e8e21c499d4328db80577d8882fa53" + integrity sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.6.tgz#54a107a3c298aee3fe5e1947a6464b9b6faca03d" + integrity sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.6" + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helpers" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.6", "@babel/generator@^7.7.2": + version "7.18.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd" + integrity sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A== + dependencies: + "@babel/types" "^7.18.7" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz#18d35bfb9f83b1293c22c55b3d576c1315b6ed96" + integrity sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg== + dependencies: + "@babel/compat-data" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" + integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q== + +"@babel/helper-function-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83" + integrity sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.6.tgz#57e3ca669e273d55c3cda55e6ebf552f37f483c8" + integrity sha512-L//phhB4al5uucwzlimruukHB3jRd5JGClwRMD/ROrVjXfLqovYnvQrK/JK36WYyVwGGO7OD3kMyVTjx+WVPhw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d" + integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg== + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.6.tgz#4c966140eaa1fcaa3d5a8c09d7db61077d4debfd" + integrity sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.6.tgz#845338edecad65ebffef058d3be851f1d28a63bc" + integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" + integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/template@^7.18.6", "@babel/template@^7.3.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" + integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/traverse@^7.18.6", "@babel/traverse@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.6.tgz#a228562d2f46e89258efa4ddd0416942e2fd671d" + integrity sha512-zS/OKyqmD7lslOtFqbscH6gMLFYOfG1YPqCKfAW5KrTeolKqvB8UelR49Fpr6y93kYkW2Ik00mT1LOGiAGvizw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.18.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.7.tgz#a4a2c910c15040ea52cdd1ddb1614a65c8041726" + integrity sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspell/cspell-bundled-dicts@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.30.0.tgz#22d0256f9b7939f53d2464a42eb9c6069adf93a4" + integrity sha512-qxc2JU34DPQrHBA7L+sW6PBtIxYcVoPm3BRy95L8NzFus8MR8HeP6KqzL4Wlm3huhFhxsxKIUQukqGDTZvoHAg== + dependencies: + "@cspell/dict-ada" "^4.0.1" + "@cspell/dict-aws" "^3.0.0" + "@cspell/dict-bash" "^4.1.1" + "@cspell/dict-companies" "^3.0.9" + "@cspell/dict-cpp" "^5.0.2" + "@cspell/dict-cryptocurrencies" "^3.0.1" + "@cspell/dict-csharp" "^4.0.2" + "@cspell/dict-css" "^4.0.5" + "@cspell/dict-dart" "^2.0.2" + "@cspell/dict-django" "^4.0.2" + "@cspell/dict-docker" "^1.1.6" + "@cspell/dict-dotnet" "^5.0.0" + "@cspell/dict-elixir" "^4.0.2" + "@cspell/dict-en-common-misspellings" "^1.0.2" + "@cspell/dict-en-gb" "1.1.33" + "@cspell/dict-en_us" "^4.3.1" + "@cspell/dict-filetypes" "^3.0.0" + "@cspell/dict-fonts" "^3.0.1" + "@cspell/dict-fullstack" "^3.1.4" + "@cspell/dict-gaming-terms" "^1.0.4" + "@cspell/dict-git" "^2.0.0" + "@cspell/dict-golang" "^6.0.1" + "@cspell/dict-haskell" "^4.0.1" + "@cspell/dict-html" "^4.0.3" + "@cspell/dict-html-symbol-entities" "^4.0.0" + "@cspell/dict-java" "^5.0.5" + "@cspell/dict-k8s" "^1.0.1" + "@cspell/dict-latex" "^4.0.0" + "@cspell/dict-lorem-ipsum" "^3.0.0" + "@cspell/dict-lua" "^4.0.1" + "@cspell/dict-node" "^4.0.2" + "@cspell/dict-npm" "^5.0.5" + "@cspell/dict-php" "^4.0.1" + "@cspell/dict-powershell" "^5.0.0" + "@cspell/dict-public-licenses" "^2.0.2" + "@cspell/dict-python" "^4.0.2" + "@cspell/dict-r" "^2.0.1" + "@cspell/dict-ruby" "^5.0.0" + "@cspell/dict-rust" "^4.0.1" + "@cspell/dict-scala" "^5.0.0" + "@cspell/dict-software-terms" "^3.1.5" + "@cspell/dict-sql" "^2.1.0" + "@cspell/dict-svelte" "^1.0.2" + "@cspell/dict-swift" "^2.0.1" + "@cspell/dict-typescript" "^3.1.1" + "@cspell/dict-vue" "^3.0.0" + +"@cspell/cspell-pipe@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-6.30.0.tgz#9c140522d4aec24656984c64b462110c058a5965" + integrity sha512-I0kT9co5c1whwd1s2PllZ86+BWl1DKRR5fxvodorsVTOUDeyP+A3ya1llo0+izo3iTbeCL2ckJpKp//tfP9vPA== + +"@cspell/cspell-service-bus@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-6.30.0.tgz#5a1d94aa8277e93fd52cced7afd5549716978c80" + integrity sha512-8X20NMsPY7RMk/1uTaRDXE+yNAE9SEmSSfhUV8wL+835MFl2GY6a0wtDL+d01vUADuqJN0wcsPH8cPNvsgycPw== + +"@cspell/cspell-types@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-6.30.0.tgz#94f63dac2f1773143e1219dbc79371c5f7094ec1" + integrity sha512-qKIMjLpZpFNFvKO6YzgoZWjAu287/AeLb2HfHUx0A0tX4Jfd8Ew8Y3CIG3I9CCsZlsXgAH1+vKTIg3wzVzzdhg== + +"@cspell/dict-ada@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-ada/-/dict-ada-4.0.1.tgz#214c91445eab16bd3fe10da5517f95bf2c90fe5f" + integrity sha512-/E9o3nHrXOhYmQE43deKbxZcR3MIJAsa+66IzP9TXGHheKEx8b9dVMVVqydDDH8oom1H0U20NRPtu6KRVbT9xw== + +"@cspell/dict-aws@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-3.0.0.tgz#7b2db82bb632c664c3d72b83267b93b9b0cafe60" + integrity sha512-O1W6nd5y3Z00AMXQMzfiYrIJ1sTd9fB1oLr+xf/UD7b3xeHeMeYE2OtcWbt9uyeHim4tk+vkSTcmYEBKJgS5bQ== + +"@cspell/dict-bash@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-bash/-/dict-bash-4.1.1.tgz#fe28016096f44d4a09fe4c5bcaf6fa40f33d98c6" + integrity sha512-8czAa/Mh96wu2xr0RXQEGMTBUGkTvYn/Pb0o+gqOO1YW+poXGQc3gx0YPqILDryP/KCERrNvkWUJz3iGbvwC2A== + +"@cspell/dict-companies@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.0.9.tgz#dfc35ad35478c8bee20a8ecd9f7509c359fe334b" + integrity sha512-wSkVIJjk33Sm3LhieNv9TsSvUSeP0R/h8xx06NqbMYF43w9J8hZiMHlbB3FzaSOHRpXT5eBIJBVTeFbceZdiqg== + +"@cspell/dict-cpp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.0.2.tgz#ab6fd2b91a08c30602426ac782a4855f239cd1e7" + integrity sha512-Q0ZjfhrHHfm0Y1/7LMCq3Fne/bhiBeBogUw4TV1wX/1tg3m+5BtaW/7GiOzRk+rFsblVj3RFam59VJKMT3vSoQ== + +"@cspell/dict-cryptocurrencies@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-3.0.1.tgz#de1c235d6427946b679d23aacff12fea94e6385b" + integrity sha512-Tdlr0Ahpp5yxtwM0ukC13V6+uYCI0p9fCRGMGZt36rWv8JQZHIuHfehNl7FB/Qc09NCF7p5ep0GXbL+sVTd/+w== + +"@cspell/dict-csharp@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-csharp/-/dict-csharp-4.0.2.tgz#e55659dbe594e744d86b1baf0f3397fe57b1e283" + integrity sha512-1JMofhLK+4p4KairF75D3A924m5ERMgd1GvzhwK2geuYgd2ZKuGW72gvXpIV7aGf52E3Uu1kDXxxGAiZ5uVG7g== + +"@cspell/dict-css@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-css/-/dict-css-4.0.5.tgz#2233138a03c163f82b0f6fbe0cdd2aada3ca4afc" + integrity sha512-z5vw8nJSyKd6d3i5UmMNoVcAp0wxvs9OHWOmAeJKT9fO3tok02gK24VZhcJ0NJtiKdHQ2zRuzdfWl51wdAiY6A== + +"@cspell/dict-dart@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-dart/-/dict-dart-2.0.2.tgz#714285f4f8bd304c1c477779ccbbfae5949819d7" + integrity sha512-jigcODm7Z4IFZ4vParwwP3IT0fIgRq/9VoxkXfrxBMsLBGGM2QltHBj7pl+joX+c4cOHxfyZktGJK1B1wFtR4Q== + +"@cspell/dict-django@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-django/-/dict-django-4.0.2.tgz#08d21ee3ce7e323e4d7634abf6d69a96a6d4930c" + integrity sha512-L0Yw6+Yh2bE9/FAMG4gy9m752G4V8HEBjEAGeRIQ9qvxDLR9yD6dPOtgEFTjv7SWlKSrLb9wA/W3Q2GKCOusSg== + +"@cspell/dict-docker@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@cspell/dict-docker/-/dict-docker-1.1.6.tgz#f84faed121e2093e3b212d19542fd27eda751c80" + integrity sha512-zCCiRTZ6EOQpBnSOm0/3rnKW1kCcAUDUA7SxJG3SuH6iZvKi3I8FEg8+O83WQUeXg0SyPNerD9F40JLnnJjJig== + +"@cspell/dict-dotnet@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-dotnet/-/dict-dotnet-5.0.0.tgz#13690aafe14b240ad17a30225ac1ec29a5a6a510" + integrity sha512-EOwGd533v47aP5QYV8GlSSKkmM9Eq8P3G/eBzSpH3Nl2+IneDOYOBLEUraHuiCtnOkNsz0xtZHArYhAB2bHWAw== + +"@cspell/dict-elixir@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-elixir/-/dict-elixir-4.0.2.tgz#1a37e92b45d744e1b78714c64811ca3dbc600a5c" + integrity sha512-/YeHlpZ1pE9VAyxp3V0xyUPapNyC61WwFuw2RByeoMqqYaIfS3Hw+JxtimOsAKVhUvgUH58zyKl5K5Q6FqgCpw== + +"@cspell/dict-en-common-misspellings@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-1.0.2.tgz#3c4ebab8e9e906d66d60f53c8f8c2e77b7f108e7" + integrity sha512-jg7ZQZpZH7+aAxNBlcAG4tGhYF6Ksy+QS5Df73Oo+XyckBjC9QS+PrRwLTeYoFIgXy5j3ICParK5r3MSSoL4gw== + +"@cspell/dict-en-gb@1.1.33": + version "1.1.33" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-gb/-/dict-en-gb-1.1.33.tgz#7f1fd90fc364a5cb77111b5438fc9fcf9cc6da0e" + integrity sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g== + +"@cspell/dict-en_us@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.1.tgz#88ae0fb555897ed77810867b849cc3495298f362" + integrity sha512-akfx/Q+4J3rfawtGaqe1Yp+fNyCGJCKmTQT14LXxGLN7DEjGvOFzlYoS+DdD3aDwAJih79bEFGiG+Lqs0zOauA== + +"@cspell/dict-filetypes@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-filetypes/-/dict-filetypes-3.0.0.tgz#3bb1ede3e28449f0d76024a7b918a556f210973a" + integrity sha512-Fiyp0z5uWaK0d2TfR9GMUGDKmUMAsOhGD5A0kHoqnNGswL2iw0KB0mFBONEquxU65fEnQv4R+jdM2d9oucujuA== + +"@cspell/dict-fonts@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-fonts/-/dict-fonts-3.0.1.tgz#0e0b875d463a9bd65e78145c9b6649ecad017df5" + integrity sha512-o2zVFKT3KcIBo88xlWhG4yOD0XQDjP7guc7C30ZZcSN8YCwaNc1nGoxU3QRea8iKcwk3cXH0G53nrQur7g9DjQ== + +"@cspell/dict-fullstack@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.1.4.tgz#930a66a1397f463c807e54dd01b0c79ec3f7fc21" + integrity sha512-OnCIn3GgAhdhsU6xMYes7/WXnbV6R/5k/zRAu/d+WZP4Ltf48z7oFfNFjHXH6b8ZwnMhpekLAnCeIfT5dcxRqw== + +"@cspell/dict-gaming-terms@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-gaming-terms/-/dict-gaming-terms-1.0.4.tgz#b67d89d014d865da6cb40de4269d4c162a00658e" + integrity sha512-hbDduNXlk4AOY0wFxcDMWBPpm34rpqJBeqaySeoUH70eKxpxm+dvjpoRLJgyu0TmymEICCQSl6lAHTHSDiWKZg== + +"@cspell/dict-git@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-git/-/dict-git-2.0.0.tgz#fa5cb298845da9c69efc01c6af07a99097718dc9" + integrity sha512-n1AxyX5Kgxij/sZFkxFJlzn3K9y/sCcgVPg/vz4WNJ4K9YeTsUmyGLA2OQI7d10GJeiuAo2AP1iZf2A8j9aj2w== + +"@cspell/dict-golang@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-golang/-/dict-golang-6.0.1.tgz#86496bac8566fa97015f62cc81e6ec96bd98500f" + integrity sha512-Z19FN6wgg2M/A+3i1O8qhrGaxUUGOW8S2ySN0g7vp4HTHeFmockEPwYx7gArfssNIruw60JorZv+iLJ6ilTeow== + +"@cspell/dict-haskell@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-haskell/-/dict-haskell-4.0.1.tgz#e9fca7c452411ff11926e23ffed2b50bb9b95e47" + integrity sha512-uRrl65mGrOmwT7NxspB4xKXFUenNC7IikmpRZW8Uzqbqcu7ZRCUfstuVH7T1rmjRgRkjcIjE4PC11luDou4wEQ== + +"@cspell/dict-html-symbol-entities@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.0.tgz#4d86ac18a4a11fdb61dfb6f5929acd768a52564f" + integrity sha512-HGRu+48ErJjoweR5IbcixxETRewrBb0uxQBd6xFGcxbEYCX8CnQFTAmKI5xNaIt2PKaZiJH3ijodGSqbKdsxhw== + +"@cspell/dict-html@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-html/-/dict-html-4.0.3.tgz#155450cb57750774583fce463d01d6323ab41701" + integrity sha512-Gae8i8rrArT0UyG1I6DHDK62b7Be6QEcBSIeWOm4VIIW1CASkN9B0qFgSVnkmfvnu1Y3H7SSaaEynKjdj3cs8w== + +"@cspell/dict-java@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-java/-/dict-java-5.0.5.tgz#c673f27ce7a5d96e205f42e8be540aeda0beef11" + integrity sha512-X19AoJgWIBwJBSWGFqSgHaBR/FEykBHTMjL6EqOnhIGEyE9nvuo32tsSHjXNJ230fQxQptEvRZoaldNLtKxsRg== + +"@cspell/dict-k8s@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.1.tgz#6c0cc521dd42fee2c807368ebfef77137686f3a1" + integrity sha512-gc5y4Nm3hVdMZNBZfU2M1AsAmObZsRWjCUk01NFPfGhFBXyVne41T7E62rpnzu5330FV/6b/TnFcPgRmak9lLw== + +"@cspell/dict-latex@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-latex/-/dict-latex-4.0.0.tgz#85054903db834ea867174795d162e2a8f0e9c51e" + integrity sha512-LPY4y6D5oI7D3d+5JMJHK/wxYTQa2lJMSNxps2JtuF8hbAnBQb3igoWEjEbIbRRH1XBM0X8dQqemnjQNCiAtxQ== + +"@cspell/dict-lorem-ipsum@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-3.0.0.tgz#c6347660fcab480b47bdcaec3b57e8c3abc4af68" + integrity sha512-msEV24qEpzWZs2kcEicqYlhyBpR0amfDkJOs+iffC07si9ftqtQ+yP3lf1VFLpgqw3SQh1M1vtU7RD4sPrNlcQ== + +"@cspell/dict-lua@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-lua/-/dict-lua-4.0.1.tgz#4c31975646cb2d71f1216c7aeaa0c5ab6994ea25" + integrity sha512-j0MFmeCouSoC6EdZTbvGe1sJ9V+ruwKSeF+zRkNNNload7R72Co5kX1haW2xLHGdlq0kqSy1ODRZKdVl0e+7hg== + +"@cspell/dict-node@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-node/-/dict-node-4.0.2.tgz#9e5f64d882568fdd2a2243542d1263dbbb87c53a" + integrity sha512-FEQJ4TnMcXEFslqBQkXa5HposMoCGsiBv2ux4IZuIXgadXeHKHUHk60iarWpjhzNzQLyN2GD7NoRMd12bK3Llw== + +"@cspell/dict-npm@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.5.tgz#fa6c1bc983e34ddc6d97094c758a4e166afd6214" + integrity sha512-eirZm4XpJNEcbmLGIwI2qXdRRlCKwEsH9mT3qCUytmbj6S6yn63F+8bShMW/yQBedV7+GXq9Td+cJdqiVutOiA== + +"@cspell/dict-php@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.1.tgz#f3c5cd241f43a32b09355370fc6ce7bd50e6402c" + integrity sha512-XaQ/JkSyq2c07MfRG54DjLi2CV+HHwS99DDCAao9Fq2JfkWroTQsUeek7wYZXJATrJVOULoV3HKih12x905AtQ== + +"@cspell/dict-powershell@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.0.tgz#c49fbefb6991f11d99ca77114b3892b6d9d3865d" + integrity sha512-UdMcc5kC6tNRBwl29RyMFa3UBPjnG7p5+8tMIZknRRU3TclxiYW6EJJhlBSYxK0V0PPe+KcEPHPD3ypshLQkOw== + +"@cspell/dict-public-licenses@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.2.tgz#81f0fde2e78bf8160ce988ae6961003a3cde3d56" + integrity sha512-baKkbs/WGEV2lCWZoL0KBPh3uiPcul5GSDwmXEBAsR5McEW52LF94/b7xWM0EmSAc/y8ODc5LnPYC7RDRLi6LQ== + +"@cspell/dict-python@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-python/-/dict-python-4.0.2.tgz#36582b21c1fda7f54d95052968b845dd595b585f" + integrity sha512-w1jSWDR1CkO23cZFbSYgnD/ZqknDZSVCI1AOE6sSszOJR8shmBkV3lMBYd+vpLsWhmkLLBcZTXDkiqFLXDGowQ== + +"@cspell/dict-r@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-r/-/dict-r-2.0.1.tgz#73474fb7cce45deb9094ebf61083fbf5913f440a" + integrity sha512-KCmKaeYMLm2Ip79mlYPc8p+B2uzwBp4KMkzeLd5E6jUlCL93Y5Nvq68wV5fRLDRTf7N1LvofkVFWfDcednFOgA== + +"@cspell/dict-ruby@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-ruby/-/dict-ruby-5.0.0.tgz#ca22ddf0842f29b485e3ef585c666c6be5227e6d" + integrity sha512-ssb96QxLZ76yPqFrikWxItnCbUKhYXJ2owkoIYzUGNFl2CHSoHCb5a6Zetum9mQ/oUA3gNeUhd28ZUlXs0la2A== + +"@cspell/dict-rust@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.1.tgz#ef0b88cb3a45265824e2c9ce31b0baa4e1050351" + integrity sha512-xJSSzHDK2z6lSVaOmMxl3PTOtfoffaxMo7fTcbZUF+SCJzfKbO6vnN9TCGX2sx1RHFDz66Js6goz6SAZQdOwaw== + +"@cspell/dict-scala@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.0.tgz#b64365ad559110a36d44ccd90edf7151ea648022" + integrity sha512-ph0twaRoV+ylui022clEO1dZ35QbeEQaKTaV2sPOsdwIokABPIiK09oWwGK9qg7jRGQwVaRPEq0Vp+IG1GpqSQ== + +"@cspell/dict-software-terms@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-3.1.5.tgz#9000ba07df6d868df257ca2438df36a0eb74acc6" + integrity sha512-wmkWHHkp2AN9EDWNBLB0VASB5OtsC3KnhoAHxCJzC6AB3xjYoBfKsvgI/o50gfbsCVQceHpqXjOEYSw/xxTKNw== + +"@cspell/dict-sql@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-sql/-/dict-sql-2.1.0.tgz#4210e83b9fc05ef91f577ae44fd264825ccfbf71" + integrity sha512-Bb+TNWUrTNNABO0bmfcYXiTlSt0RD6sB2MIY+rNlaMyIwug43jUjeYmkLz2tPkn3+2uvySeFEOMVYhMVfcuDKg== + +"@cspell/dict-svelte@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-svelte/-/dict-svelte-1.0.2.tgz#0c866b08a7a6b33bbc1a3bdbe6a1b484ca15cdaa" + integrity sha512-rPJmnn/GsDs0btNvrRBciOhngKV98yZ9SHmg8qI6HLS8hZKvcXc0LMsf9LLuMK1TmS2+WQFAan6qeqg6bBxL2Q== + +"@cspell/dict-swift@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-swift/-/dict-swift-2.0.1.tgz#06ec86e52e9630c441d3c19605657457e33d7bb6" + integrity sha512-gxrCMUOndOk7xZFmXNtkCEeroZRnS2VbeaIPiymGRHj5H+qfTAzAKxtv7jJbVA3YYvEzWcVE2oKDP4wcbhIERw== + +"@cspell/dict-typescript@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.1.tgz#25a9c241fa79c032f907db21b0aaf7c7baee6cc3" + integrity sha512-N9vNJZoOXmmrFPR4ir3rGvnqqwmQGgOYoL1+y6D4oIhyr7FhaYiyF/d7QT61RmjZQcATMa6PSL+ZisCeRLx9+A== + +"@cspell/dict-vue@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-vue/-/dict-vue-3.0.0.tgz#68ccb432ad93fcb0fd665352d075ae9a64ea9250" + integrity sha512-niiEMPWPV9IeRBRzZ0TBZmNnkK3olkOPYxC1Ny2AX4TGlYRajcW0WUtoSHmvvjZNfWLSg2L6ruiBeuPSbjnG6A== + +"@cspell/dynamic-import@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-6.30.0.tgz#73a716020e1bd0cd298817c32451b03ad2393f69" + integrity sha512-081r5p0cfHNhYidBtp6tNkZBJHUx2oM79BsBDf5Epx8ikqb6TLJMTZ5egsCCe2eXokaufwVyRiIfRLe4jKkHqQ== + dependencies: + import-meta-resolve "^2.2.2" + +"@cspell/strong-weak-map@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-6.30.0.tgz#800c34f516fba465871a0776133000680e702ad7" + integrity sha512-IBYhq9DpVElqYcrJcPjZazqPQ896cyqTrPiszuCs58roweHm1KwB2PlzFx2nerig/dwbwImKr+4QbOC6BbqnLw== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" + integrity sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" + integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== + +"@eslint/eslintrc@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d" + integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.36.0": + version "8.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" + integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== + +"@faker-js/faker@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" + integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== + +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57" + integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + +"@jest/core@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03" + integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/reporters" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.5.0" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-resolve-dependencies "^29.5.0" + jest-runner "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + jest-watcher "^29.5.0" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65" + integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ== + dependencies: + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + +"@jest/expect-utils@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.1.tgz#c1a84ee66caaef537f351dd82f7c63d559cf78d5" + integrity sha512-Tw5kUUOKmXGQDmQ9TSgTraFFS7HMC1HG/B7y0AN2G2UzjdAXz9BzK2rmNpCSDl7g7y0Gf/VLBm//blonvhtOTQ== + dependencies: + jest-get-type "^29.0.0" + +"@jest/expect-utils@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" + integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== + dependencies: + jest-get-type "^29.4.3" + +"@jest/expect@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba" + integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g== + dependencies: + expect "^29.5.0" + jest-snapshot "^29.5.0" + +"@jest/fake-timers@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c" + integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg== + dependencies: + "@jest/types" "^29.5.0" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-util "^29.5.0" + +"@jest/globals@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298" + integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/types" "^29.5.0" + jest-mock "^29.5.0" + +"@jest/reporters@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b" + integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + jest-worker "^29.5.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/source-map@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" + integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.15" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408" + integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4" + integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ== + dependencies: + "@jest/test-result" "^29.5.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + slash "^3.0.0" + +"@jest/transform@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9" + integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.1.tgz#1985650acf137bdb81710ff39a4689ec071dd86a" + integrity sha512-ft01rxzVsbh9qZPJ6EFgAIj3PT9FCRfBF9Xljo2/33VDOUjLZr0ZJ2oKANqh9S/K0/GERCsHDAQlBwj7RxA+9g== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.0.2": + version "29.0.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.2.tgz#5a5391fa7f7f41bf4b201d6d2da30e874f95b6c1" + integrity sha512-5WNMesBLmlkt1+fVkoCjHa0X3i3q8zc4QLTDkdHgCa2gyPZc7rdlZBWgVLqwS1860ZW5xJuCDwAzqbGaXIr/ew== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@joi/date@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@joi/date/-/date-2.1.0.tgz#fa7e3069a63c17d9ebe51a0b667f090c288de237" + integrity sha512-2zN5m0LgxZp/cynHGbzEImVmFIa+n+IOb/Nlw5LX/PLJneeCwG1NbiGw7MvPjsAKUGQK8z31Nn6V6lEN+4fZhg== + dependencies: + moment "2.x.x" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.15": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@lukeed/csprng@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.0.1.tgz#625e93a0edb2c830e3c52ce2d67b9d53377c6a66" + integrity sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g== + +"@nestjs/axios@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-2.0.0.tgz#2116fad483e232ef102a877b503a9f19926bd102" + integrity sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg== + +"@nestjs/cli@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-9.2.0.tgz#d174f54d7aaa6695b8e413093e3d18367bc8bec7" + integrity sha512-6B1IjDcJbrOu55oMF67L1x5lDUOZ3Zs9l7bKCBH9D78965m8wq/2rlEWl/gJto5TABLQWy3hVvV/s8VzUlRMxw== + dependencies: + "@angular-devkit/core" "15.1.4" + "@angular-devkit/schematics" "15.1.4" + "@angular-devkit/schematics-cli" "15.1.4" + "@nestjs/schematics" "^9.0.0" + chalk "3.0.0" + chokidar "3.5.3" + cli-table3 "0.6.3" + commander "4.1.1" + fork-ts-checker-webpack-plugin "7.3.0" + inquirer "7.3.3" + node-emoji "1.11.0" + ora "5.4.1" + os-name "4.0.1" + rimraf "4.1.2" + shelljs "0.8.5" + source-map-support "0.5.21" + tree-kill "1.2.2" + tsconfig-paths "4.1.2" + tsconfig-paths-webpack-plugin "4.0.0" + typescript "4.9.5" + webpack "5.75.0" + webpack-node-externals "3.0.0" + +"@nestjs/common@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.3.10.tgz#c137402cad41123eaf5c74c2404e92490f9bbd50" + integrity sha512-wj2bM9TXBlAvzgznkID0s7bN/niVn90sZIDtRFDnvaB1qagEpkWA0Bt39qilIuqdReluIaCjeEW106U0oyz+mQ== + dependencies: + uid "2.0.1" + iterare "1.2.1" + tslib "2.5.0" + +"@nestjs/config@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.3.1.tgz#6ac151f818db4ccf987c7ff8ef5b2c1f4eeec913" + integrity sha512-Ckzel0NZ9CWhNsLfE1hxfDuxJuEbhQvGxSlmZ1/X8awjRmAA/g3kT6M1+MO1SHj1wMtPyUfd9WpwkiqFbiwQgA== + dependencies: + dotenv "16.0.3" + dotenv-expand "10.0.0" + lodash "4.17.21" + uuid "9.0.0" + +"@nestjs/core@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.3.10.tgz#34549387f60d3a6c2a5cc438f0f6a55592c65914" + integrity sha512-9QuE5jHtRnqKZULUhQoB0pNU26mwJ4hYNwAQ7lc/nFU2/4Ci+wTTRrXhLZeirgaF6TLCgLQB7/wLHImcfoXUog== + dependencies: + uid "2.0.1" + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + path-to-regexp "3.2.0" + tslib "2.5.0" + +"@nestjs/jwt@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.0.2.tgz#bc049fe1622299e456a849999ebf30cb56ab4dd1" + integrity sha512-MLxjCSbO7C9fN2hst5kpIhnJAgglJmrKppXAXqElB8A9ip3ZuCowMDjjmNWWJyfOzE98NV0E0iEQGE2StMUC+Q== + dependencies: + "@types/jsonwebtoken" "9.0.1" + jsonwebtoken "9.0.0" + +"@nestjs/mapped-types@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz#d9ddb143776e309dbc1a518ac1607fddac1e140e" + integrity sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg== + +"@nestjs/mongoose@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/mongoose/-/mongoose-9.2.1.tgz#b880feb7ce52a081e81374ddf3c3ed0eff1224f4" + integrity sha512-tMK5kKFjQnNVhqJDw1wa352z+VsODOFznTn74xSzrziof03qS+O6rLU4q1kMx0B4AmFbADf03GOdpvBc9bMWqw== + +"@nestjs/passport@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-9.0.3.tgz#4df0e6de3176e04a5770cb432e58f129c8e49f9e" + integrity sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg== + +"@nestjs/platform-express@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.3.10.tgz#5493bd4dc3f5f28e3224afd56d113017b746f3d6" + integrity sha512-5aWokr8s0pipD5c/n40xC1iv3cMXfWrOhciX430p53cy4uyTAE+sTBk0PhB6tdG8NpK33aNqqHz/tyKlauQu/Q== + dependencies: + body-parser "1.20.2" + cors "2.8.5" + express "4.18.2" + multer "1.4.4-lts.1" + tslib "2.5.0" + +"@nestjs/schedule@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-2.2.0.tgz#6e6e55648d9fa03dfc861354d00da2985161753b" + integrity sha512-wrDnUONTxBkD6lTWh9ecYk/kvJTbA3PylotjBoRsECmcS1SNvgInFXuL38UnHiFnXM3CHSFnzRLB259Bc1mOdQ== + dependencies: + cron "2.2.0" + uuid "9.0.0" + +"@nestjs/schematics@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.1.tgz#2ec1b3fc4cd2c44310d4d7d1f5f276a18d24964b" + integrity sha512-QU7GbnQvADFXdumcdADmv4vil3bhnYl2IFHWKieRt0MgIhghgBxIB7kDKWhswcuZ0kZztVbyYjo9aCrlf62fcw== + dependencies: + "@angular-devkit/core" "14.0.5" + "@angular-devkit/schematics" "14.0.5" + fs-extra "10.1.0" + jsonc-parser "3.0.0" + pluralize "8.0.0" + +"@nestjs/schematics@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.4.tgz#ab612f5a8e006ca1d617eddc8143ee00b766312b" + integrity sha512-egurCfAc4e5i1r2TmeAF0UrOKejFmT5oTdv4b7HcOVPupc3QGU7CbEfGleL3mkM5AjrixTQeMxU9bJ00ttAbGg== + dependencies: + "@angular-devkit/core" "15.0.4" + "@angular-devkit/schematics" "15.0.4" + fs-extra "11.1.0" + jsonc-parser "3.2.0" + pluralize "8.0.0" + +"@nestjs/swagger@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-6.2.1.tgz#38caa76ac00993ddc11a79dd24ab9f4392d2791d" + integrity sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg== + dependencies: + "@nestjs/mapped-types" "1.2.2" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.2.0" + swagger-ui-dist "4.15.5" + +"@nestjs/terminus@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/terminus/-/terminus-9.2.1.tgz#f173ba807bbab6ed2ee892e859455553274a725e" + integrity sha512-bPJsxKzqLl1BIs1YFIji20h42VG4ElGqc+lyw7nW+as0DkfjpRYUdyEBQJo6dTAcqRrVxSN2m3wKweBknK3Nxw== + dependencies: + boxen "5.1.2" + check-disk-space "3.3.1" + +"@nestjs/testing@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-9.3.10.tgz#d0229d4d338806758dae824bdbeb935e097d4901" + integrity sha512-TGspJkzDx1YmJzlmNG5WrhFa7IGgXbCVt4UXvBVqEk2QRPmJFZnqd0T9waKZ+SxwH4gY5sdw2niTFvOgqGVfJw== + dependencies: + tslib "2.5.0" + +"@nestjs/throttler@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-4.0.0.tgz#9f9c66df62da6ec1b1ad4305e6a3865676e3812f" + integrity sha512-2T2S/hFhxROa/PZRZhHWFkrukxg3T8Db32Y04m6U6j6N2XqFGSKXhjfIbORO8kk/S2jswa9oTX/K12E120tgaQ== + dependencies: + md5 "^2.2.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.24.1": + version "0.24.19" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.19.tgz#5297278e0d8a1aea084685a3216074910ac6c113" + integrity sha512-gHJu8cdYTD5p4UqmQHrxaWrtb/jkH5imLXzuBypWhKzNkW0qfmgz+w1xaJccWVuJta1YYUdlDiPHXRTR4Ku0MQ== + +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + +"@ts-morph/common@~0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.3.tgz#a96e250217cd30e480ab22ec6a0ebbe65fd784ff" + integrity sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w== + dependencies: + fast-glob "^3.2.7" + minimatch "^3.0.4" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.17.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" + integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== + dependencies: + "@babel/types" "^7.3.0" + +"@types/bcryptjs@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" + integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bytes@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.1.tgz#67a876422e660dc4c10a27f3e5bcfbd5455f01d0" + integrity sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w== + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + +"@types/cors@^2.8.13": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/cron@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/cron/-/cron-2.0.0.tgz#4fe75f2720a3b69a1f7b80e656749f4c2c96d727" + integrity sha512-xZM08fqvwIXgghtPVkSPKNgC+JoMQ2OHazEvyTKnNf7aWu1aB6/4lBbQFrb03Td2cUGG7ITzMv3mFYnMu6xRaQ== + dependencies: + "@types/luxon" "*" + "@types/node" "*" + +"@types/crypto-js@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.5" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" + integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.52" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.52.tgz#7f1f57ad5b741f3d5b210d3b1f145640d89bf8fe" + integrity sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@^4.17.18": + version "4.17.29" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.3": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@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.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.4.4": + version "29.4.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.4.tgz#ba257bd7b1876dec9e0b4fb82fa60eec5505e5f1" + integrity sha512-qezb65VIH7X1wobSnd6Lvdve7PXSyQRa3dljTkhTtDhi603RvHQCshSlJcuyMLHJpeHgY3NKwvDJWxMOOHxGDQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/jsonwebtoken@*": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + +"@types/jsonwebtoken@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== + dependencies: + "@types/node" "*" + +"@types/lodash@^4.14.191": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + +"@types/luxon@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.3.2.tgz#8a3f2cdd4858ce698b56cd8597d9243b8e9d3c65" + integrity sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/morgan@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.4.tgz#99965ad2bdc7c5cee28d8ce95cfa7300b19ea562" + integrity sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ== + dependencies: + "@types/node" "*" + +"@types/ms@^0.7.31": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + +"@types/node@*": + version "18.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" + integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + +"@types/node@^18.15.3": + version "18.15.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" + integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/passport-jwt@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.8.tgz#c8a95bf7d8f330f2560f1b3d07605e23ac01469a" + integrity sha512-VKJZDJUAHFhPHHYvxdqFcc5vlDht8Q2pL1/ePvKAgqRThDaCc84lSYOTQmnx3+JIkDlN+2KfhFhXIzlcVT+Pcw== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c" + integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.9.tgz#b32fa8f7485dace77a9b58e82d0c92908f6e8387" + integrity sha512-9+ilzUhmZQR4JP49GdC2O4UdDE3POPLwpmaTC/iLkW7l0TZCXOo1zsTnnlXPq6rP1UsUZPfbAV4IUdiwiXyC7g== + dependencies: + "@types/express" "*" + +"@types/prettier@^2.1.5": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" + integrity sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/response-time@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/response-time/-/response-time-2.3.5.tgz#e85ff348caefd0f8d3e8902424c681a59aafc31e" + integrity sha512-4ANzp+I3K7sztFFAGPALWBvSl4ayaDSKzI2Bok+WNz+en2eB2Pvk6VCjR47PBXBWOkEg2r4uWpZOlXA5DNINOQ== + dependencies: + "@types/express" "*" + "@types/node" "*" + +"@types/semver@^7.3.12": + version "7.3.12" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c" + integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== + +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/superagent@*": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + +"@types/uuid@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + +"@types/validator@^13.7.10": + version "13.7.10" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7" + integrity sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ== + +"@types/webidl-conversions@*": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e" + integrity sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q== + +"@types/whatwg-url@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" + integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== + dependencies: + "@types/node" "*" + "@types/webidl-conversions" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz#bc2400c3a23305e8c9a9c04aa40933868aaaeb47" + integrity sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/type-utils" "5.55.0" + "@typescript-eslint/utils" "5.55.0" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.55.0.tgz#8c96a0b6529708ace1dcfa60f5e6aec0f5ed2262" + integrity sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw== + dependencies: + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/typescript-estree" "5.55.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz#e863bab4d4183ddce79967fe10ceb6c829791210" + integrity sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw== + dependencies: + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/visitor-keys" "5.55.0" + +"@typescript-eslint/type-utils@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz#74bf0233523f874738677bb73cb58094210e01e9" + integrity sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA== + dependencies: + "@typescript-eslint/typescript-estree" "5.55.0" + "@typescript-eslint/utils" "5.55.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.55.0.tgz#9830f8d3bcbecf59d12f821e5bc6960baaed41fd" + integrity sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug== + +"@typescript-eslint/typescript-estree@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz#8db7c8e47ecc03d49b05362b8db6f1345ee7b575" + integrity sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ== + dependencies: + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/visitor-keys" "5.55.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.55.0.tgz#34e97322e7ae5b901e7a870aabb01dad90023341" + integrity sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/typescript-estree" "5.55.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz#01ad414fca8367706d76cdb94adf788dc5b664a2" + integrity sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw== + dependencies: + "@typescript-eslint/types" "5.55.0" + eslint-visitor-keys "^3.3.0" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accept-language-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" + integrity sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +acorn@^8.7.1, acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + +ajv-formats@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@8.11.0, ajv@^8.0.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + +array-timsort@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926" + integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +babel-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" + integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q== + dependencies: + "@jest/transform" "^29.5.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + +boxen@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.14.5, browserslist@^4.20.2: + version "4.21.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.1.tgz#c9b9b0a54c7607e8dc3e01a0d311727188011a00" + integrity sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ== + dependencies: + caniuse-lite "^1.0.30001359" + electron-to-chromium "^1.4.172" + node-releases "^2.0.5" + update-browserslist-db "^1.0.4" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +bson@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.0.1.tgz#4cd3eeeabf6652ef0d6ab600f9a18212d39baac3" + integrity sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0, callsites@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001359: + version "1.0.30001363" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" + integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== + +cfb@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + +chalk@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +check-disk-space@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.3.1.tgz#10c4c8706fdd16d3e5c3572a16aa95efd0b4d40b" + integrity sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ== + +chokidar@3.5.3, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" + integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" + integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== + dependencies: + "@types/validator" "^13.7.10" + libphonenumber-js "^1.10.14" + validator "^13.7.0" + +clear-module@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/clear-module/-/clear-module-4.1.2.tgz#5a58a5c9f8dccf363545ad7284cad3c887352a80" + integrity sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw== + dependencies: + parent-module "^2.0.0" + resolve-from "^5.0.0" + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + +cli-table3@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +code-block-writer@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.1.tgz#53920acb587af3f7be57101c0248d98f372d73ac" + integrity sha512-0ch9DeCY8v/BWA9n1/Qu1ALG3lpesel4PYL2eNlGLgvGl+J7k74i+dSXSF3wLvF5SYII8/GUT/Ic+fycBR/DUQ== + +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1" + integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +comment-json@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365" + integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw== + 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" + +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0, cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookiejar@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + +core-util-is@^1.0.3, core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz#947e174c796483ccf0a48476c24e4fefb7e1aea8" + integrity sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + +crc-32@~1.2.0, crc-32@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cron@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cron/-/cron-2.2.0.tgz#605ae5048e9715a22db12e9fe2563a92740b9fed" + integrity sha512-GPiI3OgMv83XRtEUc2gUdaLvJhO3XbLN288layOBkDTupg0RK5IECNGpkykIMHg+muVR2bxt29b0xvCAcBrjYQ== + dependencies: + luxon "^3.2.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +cspell-dictionary@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-6.30.0.tgz#ac2cfefa8f1db728a68ec6811d08c5296ba84b29" + integrity sha512-PY25Lrc3VC3UOtfpelBuwGilMWRD0zGa56uF9Gw+S1eAsa1eYug+Jz7/vAAUNJI91BCmF0dV9RU2LKlPNPxwaA== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + cspell-trie-lib "6.30.0" + fast-equals "^4.0.3" + gensequence "^5.0.2" + +cspell-gitignore@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-6.30.0.tgz#f7bb3ebd78e2b586b6f44d4da09f52f2126e4939" + integrity sha512-nPW7x1y4LsVgcC4dO5Tt7BKd1lXj/rul4RRoy7RQVORn/bnzOkpSeLaFp9q+4lGeC2I+p8i8brZImanLLftZ1w== + dependencies: + cspell-glob "6.30.0" + find-up "^5.0.0" + +cspell-glob@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-6.30.0.tgz#ca9613182ad312b87e6aed6419844e24a35427e5" + integrity sha512-FHlRxn7olHiks/LMoPadL5kPuu+aPcpm9OPIF5dF/comMEzQrOG7SA6AoTuQmD7pIUv+94zzjAKOmwEfz6/fvA== + dependencies: + micromatch "^4.0.5" + +cspell-grammar@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-6.30.0.tgz#dbf81502a14db6649a7ad686a5762a8952201eac" + integrity sha512-vOM5VpSknf6HNoUqqaNK47Aez1ORTPC9r6vEyjC1Uh9t6R5jGUCm0CdKI+ABKiEdPi44IETrC7lpFLIdGmQziQ== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + +cspell-io@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-6.30.0.tgz#851458f007eed4c776ac8b88b653171f3ccce826" + integrity sha512-y8XtfjPYBvzf87y6zsFiKuAYx/mKbWk9ACZc4Tt3pvDmGkf3anGjLJLfg6360ObGymuYwT4Jt2Al+gWsDpE6vQ== + dependencies: + "@cspell/cspell-service-bus" "6.30.0" + node-fetch "^2.6.9" + +cspell-lib@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-6.30.0.tgz#daefa8bfd08c5045428f876c7c739328c0a9b87b" + integrity sha512-88wuGw93dICcKD2Zu9w1XvwAj5AQ/EzXleo5Eab+6G2zUjHk0zXCJorivrWXWuwnN8WePAp4mKoVUI+eXpDksw== + dependencies: + "@cspell/cspell-bundled-dicts" "6.30.0" + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + "@cspell/strong-weak-map" "6.30.0" + clear-module "^4.1.2" + comment-json "^4.2.3" + configstore "^5.0.1" + cosmiconfig "^8.1.0" + cspell-dictionary "6.30.0" + cspell-glob "6.30.0" + cspell-grammar "6.30.0" + cspell-io "6.30.0" + cspell-trie-lib "6.30.0" + fast-equals "^4.0.3" + find-up "^5.0.0" + gensequence "^5.0.2" + import-fresh "^3.3.0" + resolve-from "^5.0.0" + resolve-global "^1.0.0" + vscode-languageserver-textdocument "^1.0.8" + vscode-uri "^3.0.7" + +cspell-trie-lib@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-6.30.0.tgz#1e1aa4fdaa0b894b1418c6214ea13dc826a37180" + integrity sha512-drl5vSm7JJvlchxfd4tYR9EGKj4a1ga2oqqKKPq7oh9BukN3UW03LdYYjjn3ou1By8bnZHvTSy1M06rHrtNxmA== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + gensequence "^5.0.2" + +cspell@^6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell/-/cspell-6.30.0.tgz#5bd7fd8bbc5c864b7c22ba834f7531dfb408587a" + integrity sha512-jv89wDPPhXMVVyzOiQWRq2458QnoUaVrse2oXWky13UNVcHrsXq5GS4gQtKjAVxvwSJcVZpwtN59eyulrqAFpw== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/dynamic-import" "6.30.0" + chalk "^4.1.2" + commander "^10.0.0" + cspell-gitignore "6.30.0" + cspell-glob "6.30.0" + cspell-io "6.30.0" + cspell-lib "6.30.0" + fast-glob "^3.2.12" + fast-json-stable-stringify "^2.1.0" + file-entry-cache "^6.0.1" + get-stdin "^8.0.0" + imurmurhash "^0.1.4" + semver "^7.3.8" + strip-ansi "^6.0.1" + vscode-uri "^3.0.7" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA== + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +diff-sequences@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f" + integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA== + +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dotenv-expand@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.172: + version "1.4.182" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.182.tgz#5d59214ebfe90b36f23e81cd226a42732cd8c677" + integrity sha512-OpEjTADzGoXABjqobGhpy0D2YsTncAax7IkER68ycc4adaq0dqEG9//9aenKPy7BGA90bqQdLac0dPp6uMkcSg== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0, enhanced-resolve@^5.7.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.19.0, es-abstract@^1.19.5: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz#f1cc58a8afebc50980bd53475451df146c13182d" + integrity sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA== + +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== + dependencies: + debug "^3.2.7" + is-core-module "^2.11.0" + resolve "^1.22.1" + +eslint-module-utils@^2.7.4: + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.27.5: + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" + has "^1.0.3" + is-core-module "^2.11.0" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" + tsconfig-paths "^3.14.1" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.36.0: + version "8.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" + integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.1" + "@eslint/js" "8.36.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-visitor-keys "^3.3.0" + espree "^9.5.0" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113" + integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.2.tgz#c6d3fee05dd665808e2ad870631f221f5617b1d1" + integrity sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0: + version "29.0.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.1.tgz#a2fa64a59cffe4b4007877e730bc82be3d1742bb" + integrity sha512-yQgemsjLU+1S8t2A7pXT3Sn/v5/37LY8J+tocWtKEA0iEYYc6gfKbbJJX2fxHZmd7K9WpdbQqXUpmYkq1aewYg== + dependencies: + "@jest/expect-utils" "^29.0.1" + jest-get-type "^29.0.0" + jest-matcher-utils "^29.0.1" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + +expect@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7" + integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== + dependencies: + "@jest/expect-utils" "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-equals@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" + integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== + +fast-glob@^3.2.12: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-xml-parser@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz#5a98c18238d28a57bbdfa9fe4cda01211fff8f4a" + integrity sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg== + dependencies: + strnum "^1.0.5" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-stream-rotator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz#007019e735b262bb6c6f0197e58e5c87cb96cec3" + integrity sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ== + dependencies: + moment "^2.29.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" + integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +fork-ts-checker-webpack-plugin@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz#a9c984a018493962360d7c7e77a67b44a2d5f3aa" + integrity sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + node-abort-controller "^3.0.1" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.1.tgz#81269cbea1a613240049f5f61a9d97731517414f" + integrity sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@10.1.0, fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed" + integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensequence@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/gensequence/-/gensequence-5.0.2.tgz#f065be2f9a5b2967b9cad7f33b2d79ce1f22dc82" + integrity sha512-JlKEZnFc6neaeSVlkzBGGgkIoIaSxMgvdamRoPN8r3ozm2r9dusqxeKqYQ7lhzmj2UhFQP8nkyfCaiLQxiLrDA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +geolib@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/geolib/-/geolib-3.3.3.tgz#17f5a0dcdc0b051bd631b66f7131d2c14c54a15b" + integrity sha512-YO704pzdB/8QQekQuDmFD5uv5RAwAf4rOUPdcMhdEOz+HoPWD0sC7Qqdwb+LAvwIjXVRawx0QgZlocKYh8PFOQ== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^9.2.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.2.1.tgz#f47e34e1119e7d4f93a546e75851ba1f1e68de50" + integrity sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + +global-dirs@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg== + dependencies: + ini "^1.3.4" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-own-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af" + integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +helmet@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-6.0.1.tgz#52ec353638b2e87f14fe079d142b368ac11e79a4" + integrity sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw== + +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +husky@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +import-meta-resolve@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz#75237301e72d1f0fbd74dbc6cca9324b164c2cc9" + integrity sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-core-module@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-core-module@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1, iterare@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317" + integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.5.0" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + p-limit "^3.1.0" + pretty-format "^29.5.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67" + integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw== + dependencies: + "@jest/core" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da" + integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.5.0" + "@jest/types" "^29.5.0" + babel-jest "^29.5.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.5.0" + jest-environment-node "^29.5.0" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-runner "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.1.tgz#d14e900a38ee4798d42feaaf0c61cb5b98e4c028" + integrity sha512-l8PYeq2VhcdxG9tl5cU78ClAlg/N7RtVSp0v3MlXURR0Y99i6eFnegmasOandyTmO6uEdo20+FByAjBFEO9nuw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.0.0" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-diff@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" + integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06" + integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA== + dependencies: + "@jest/types" "^29.5.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + jest-util "^29.5.0" + pretty-format "^29.5.0" + +jest-environment-node@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967" + integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + jest-util "^29.5.0" + +jest-get-type@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" + integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== + +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + +jest-haste-map@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de" + integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA== + dependencies: + "@jest/types" "^29.5.0" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + jest-worker "^29.5.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c" + integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow== + dependencies: + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-matcher-utils@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.1.tgz#eaa92dd5405c2df9d31d45ec4486361d219de3e9" + integrity sha512-/e6UbCDmprRQFnl7+uBKqn4G22c/OmwriE5KCMVqxhElKCQUDcFnq5XM9iJeKtzy4DUjxT27y9VHmKPD8BQPaw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.0.1" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-matcher-utils@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" + integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-message-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.1.tgz#85c4b5b90296c228da158e168eaa5b079f2ab879" + integrity sha512-wRMAQt3HrLpxSubdnzOo68QoTfQ+NLXFzU0Heb18ZUzO2S9GgaXNEdQ4rpd0fI9dq2NXkpCk1IUWSqzYKji64A== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.0.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.0.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" + integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.5.0" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-util "^29.5.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4" + integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.5.0" + +jest-resolve@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc" + integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.5.0" + jest-validate "^29.5.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8" + integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/environment" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.4.3" + jest-environment-node "^29.5.0" + jest-haste-map "^29.5.0" + jest-leak-detector "^29.5.0" + jest-message-util "^29.5.0" + jest-resolve "^29.5.0" + jest-runtime "^29.5.0" + jest-util "^29.5.0" + jest-watcher "^29.5.0" + jest-worker "^29.5.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420" + integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/globals" "^29.5.0" + "@jest/source-map" "^29.4.3" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce" + integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.5.0" + graceful-fs "^4.2.9" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + natural-compare "^1.4.0" + pretty-format "^29.5.0" + semver "^7.3.5" + +jest-util@^29.0.0: + version "29.0.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.2.tgz#c75c5cab7f3b410782f9570a60c5558b5dfb6e3a" + integrity sha512-ozk8ruEEEACxqpz0hN9UOgtPZS0aN+NffwQduR5dVlhN+eN47vxurtvgZkYZYMpYrsmlAEx1XabkB3BnN0GfKQ== + dependencies: + "@jest/types" "^29.0.2" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.1.tgz#f854a4a8877c7817316c4afbc2a851ceb2e71598" + integrity sha512-GIWkgNfkeA9d84rORDHPGGTFBrRD13A38QVSKE0bVrGSnoR1KDn8Kqz+0yI5kezMgbT/7zrWaruWP1Kbghlb2A== + dependencies: + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc" + integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ== + dependencies: + "@jest/types" "^29.5.0" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + leven "^3.1.0" + pretty-format "^29.5.0" + +jest-watcher@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363" + integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA== + dependencies: + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.5.0" + string-length "^4.0.1" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d" + integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== + dependencies: + "@types/node" "*" + jest-util "^29.5.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e" + integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ== + dependencies: + "@jest/core" "^29.5.0" + "@jest/types" "^29.5.0" + import-local "^3.0.2" + jest-cli "^29.5.0" + +joi@^17.8.4: + version "17.8.4" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.4.tgz#f2d91ab8acd3cca4079ba70669c65891739234aa" + integrity sha512-jjdRHb5WtL+KgSHvOULQEPPv4kcl+ixd1ybOFQq3rWLgEEqc03QMmilodL0GVJE14U/SQDXkUhQUSZANGDH/AA== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +js-sdsl@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" + integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.3, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonc-parser@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonc-parser@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonwebtoken@9.0.0, jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kareem@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" + integrity sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +libphonenumber-js@^1.10.14: + version "1.10.15" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.15.tgz#cad454adb5bf271bc820bbf7dd66776afcda7be6" + integrity sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.compact@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5" + integrity sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ== + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +logform@^2.3.2, logform@^2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.2.tgz#a617983ac0334d0c3b942c34945380062795b47c" + integrity sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.1.tgz#4716408dec51d5d0104732647f584d1f6738b109" + integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== + +luxon@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" + integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== + +macos-release@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2" + integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g== + +magic-string@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.1.tgz#ba9b651354fa9512474199acecf9c6dbe93f97fd" + integrity sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@0.26.7: + version "0.26.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" + integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.7.tgz#e5252ad2242a724f938cb937e3c4f7ceb1f70e5a" + integrity sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw== + dependencies: + fs-monkey "^1.0.3" + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^7.4.1: + version "7.4.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.2.tgz#157e847d79ca671054253b840656720cb733f10f" + integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minipass@^4.0.2, minipass@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.4.tgz#7d0d97434b6a19f59c5c3221698b48bbf3b2cd06" + integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@2.x.x, moment@^2.29.1, moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + +mongodb-connection-string-url@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" + integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== + dependencies: + "@types/whatwg-url" "^8.2.1" + whatwg-url "^11.0.0" + +mongodb@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.1.0.tgz#e551f9e496777bde9173e51d16c163ab2c805b9d" + integrity sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw== + dependencies: + bson "^5.0.1" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + saslprep "^1.0.3" + +mongoose@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.0.2.tgz#679df6bae18abb7a27f412c4e9e7285363cd07eb" + integrity sha512-whX+5lAOLOs6VXRr9w+6m5qb8m/IXWLLb9+0/HRUh2TiIYtTt7UvajK92zW6wllCjBkrrnz/MDIOTCWMbs8K4g== + dependencies: + bson "^5.0.1" + kareem "2.5.1" + mongodb "5.1.0" + mpath "0.9.0" + mquery "5.0.0" + ms "2.1.3" + sift "16.0.1" + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +mpath@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" + integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew== + +mquery@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-5.0.0.tgz#a95be5dfc610b23862df34a47d3e5d60e110695d" + integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg== + dependencies: + debug "4.x" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nest-winston@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/nest-winston/-/nest-winston-1.9.1.tgz#2968a9553ca60728be93e47ccb659a8946e97481" + integrity sha512-L3fjIfas+7ZziKsCQiTYvTVw0Hpv3oN4TDnbLLYLIQgKLNpzQyf/2yZv1/buDPMrGJKrNPwiHLoifjGHon34+A== + dependencies: + fast-safe-stringify "^2.1.1" + +nestjs-command@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/nestjs-command/-/nestjs-command-3.1.3.tgz#98149d00d32232a2d24cc22f12c13b856753a784" + integrity sha512-RWPk9q4Z14KoLReLJz/ehQ/60ezCxCJc2ko4DnuUDJ3w30ZbtNVEE7CRHfbbl5QepSHDBj8ydJreHVGA73YKng== + dependencies: + lodash.compact "^3.0.1" + lodash.flattendeep "^4.4.0" + +nestjs-i18n@^10.2.6: + version "10.2.6" + resolved "https://registry.yarnpkg.com/nestjs-i18n/-/nestjs-i18n-10.2.6.tgz#ea15e863731036ddd5f4eb5068c56dab1355ebdc" + integrity sha512-vWjOXNz3ohJcKybtgdCWruNqreNOkVGfbTzGFxxdZ6Y9VfVJERT2+/30KAcBwJTpjuStoTVhMpILAKkXMk8KLQ== + dependencies: + accept-language-parser "^1.5.0" + chokidar "^3.5.3" + cookie "^0.5.0" + iterare "^1.2.1" + js-yaml "^4.1.0" + string-format "^2.0.0" + +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.0, npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-inspect@^1.12.2: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1, on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-name@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" + integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== + dependencies: + macos-release "^2.5.0" + windows-release "^4.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parent-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-2.0.0.tgz#fa71f88ff1a50c27e15d8ff74e0e3a9523bf8708" + integrity sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg== + dependencies: + callsites "^3.1.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-headerapikey@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz#b71960523999c9864151b8535c919e3ff5ba75ce" + integrity sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA== + dependencies: + lodash "^4.17.15" + passport-strategy "^1.0.0" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.6.0.tgz#e869579fab465b5c0b291e841e6cc95c005fac9d" + integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.1.tgz#dab45f7bb1d3f45a0e271ab258999f4ab7e23132" + integrity sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA== + dependencies: + lru-cache "^7.14.1" + minipass "^4.0.2" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pluralize@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^2.8.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + +pretty-format@^29.0.0, pretty-format@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.1.tgz#2f8077114cdac92a59b464292972a106410c7ad0" + integrity sha512-iTHy3QZMzuL484mSTYbQIM1AHhEQsH8mXWS2/vd2yFBYnG3EBqGiMONo28PlPgrW7P/8s/1ISv+y7WH306l8cw== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" + integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== + dependencies: + "@jest/schemas" "^29.4.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pure-rand@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af" + integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== + +qs@6.11.0, qs@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-global@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" + integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== + dependencies: + global-dirs "^0.1.1" + +resolve.exports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" + integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + +resolve@^1.1.6, resolve@^1.20.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +response-time@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/response-time/-/response-time-2.3.2.tgz#ffa71bab952d62f7c1d49b7434355fbc68dffc5a" + integrity sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw== + dependencies: + depd "~1.1.0" + on-headers "~1.0.1" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" + integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.0.tgz#c7a9f45bb2ec058d2e60ef9aca5167974313d605" + integrity sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ== + dependencies: + glob "^9.2.0" + +rotating-file-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-3.1.0.tgz#6cf50e1671de82a396de6d31d39a6f2445f45fba" + integrity sha512-TkMF6cP1/QDcon9D71mjxHoflNuznNOrY5JJQfuxkKklZRmoow/lWBLNxXVjb6KcjAU8BDCV145buLgOx9Px1Q== + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@6.6.7, rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + +rxjs@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saslprep@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@7.x, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +sift@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" + integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== + +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +superagent@^8.0.5: + version "8.0.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.6.tgz#e3fb0b3112b79b12acd605c08846253197765bf6" + integrity sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-ui-dist@4.15.5: + version "4.15.5" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz#cda226a79db2a9192579cc1f37ec839398a62638" + integrity sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA== + +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90" + integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.7" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.7.2" + +terser@^5.7.2: + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity "sha1-msnyKwaZTXNhdPQJGqNo24lvHBA= sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==" + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +"true-myth@^4.1.0": + version "4.1.1" + resolved "https://registry.yarnpkg.com/true-myth/-/true-myth-4.1.1.tgz#ff4ac9d5130276e34aa338757e2416ec19248ba2" + integrity sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg== + +ts-jest@^29.0.5: + version "29.0.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" + integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + +ts-loader@^9.4.2: + version "9.4.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" + integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-morph@^13.0.1: + version "13.0.3" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.3.tgz#c0c51d1273ae2edb46d76f65161eb9d763444c1d" + integrity sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw== + dependencies: + "@ts-morph/common" "~0.12.3" + code-block-writer "^11.0.0" + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +ts-prune@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38" + integrity sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw== + dependencies: + commander "^6.2.1" + cosmiconfig "^7.0.1" + json5 "^2.1.3" + lodash "^4.17.21" + "true-myth" "^4.1.0" + ts-morph "^13.0.1" + +tsconfig-paths-webpack-plugin@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz#84008fc3e3e0658fdb0262758b07b4da6265ff1a" + integrity sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.0.0" + +tsconfig-paths@4.1.2, tsconfig-paths@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz#4819f861eef82e6da52fb4af1e8c930a39ed979a" + integrity sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz#f8ef7d467f08ae3a695335bf1ece088c5538d2c1" + integrity sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow== + dependencies: + json5 "^2.2.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.1.0, tslib@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +typescript@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5" + integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw== + +ua-parser-js@^1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.34.tgz#b33f41c415325839f354005d25a2f588be296976" + integrity sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew== + +uid@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.1.tgz#a3f57c962828ea65256cd622fc363028cdf4526b" + integrity sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A== + dependencies: + "@lukeed/csprng" "^1.0.0" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz#dbfc5a789caa26b1db8990796c2c8ebbce304824" + integrity sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vscode-languageserver-textdocument@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" + integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== + +vscode-uri@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" + integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.75.0: + version "5.75.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" + integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +windows-release@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" + integrity sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== + dependencies: + execa "^4.0.2" + +winston-daily-rotate-file@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz#f60a643af87f8867f23170d8cd87dbe3603a625f" + integrity sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA== + dependencies: + file-stream-rotator "^0.6.1" + object-hash "^2.0.1" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +winston-transport@^4.4.0, winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50" + integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew== + dependencies: + "@colors/colors" "1.5.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xlsx@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0" + integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + dependencies: + adler-32 "~1.3.0" + cfb "~1.2.1" + codepage "~1.15.0" + crc-32 "~1.2.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@21.1.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-parser@^21.0.0, yargs-parser@^21.0.1: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + +yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + +yargs@^17.7.1: + version "17.7.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" + integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yarn@^1.22.19: + version "1.22.19" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.19.tgz#4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" + integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==