diff --git a/.gitignore b/.gitignore index ea48d65..f76c6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,11 @@ lerna-debug.log* !.vscode/launch.json !.vscode/extensions.json -.env \ No newline at end of file +# Environments +.env +.env.local +.env.development + +# Keys +.keys/* +!.keys/.gitkeep diff --git a/.keys/.gitkeep b/.keys/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000..dbdd55b --- /dev/null +++ b/.swcrc @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "sourceMaps": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "baseUrl": "./" + }, + "minify": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d7b85f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "fileTreeExtractor.ignoredBy": "both" +} diff --git a/README.md b/README.md index 8ff5574..44e6709 100644 --- a/README.md +++ b/README.md @@ -2,56 +2,60 @@ This is a starter kit for building a Nest.js application with MongoDB, Express, Clustering, Swagger, Pino, and Exception handling. - ## Getting Started + To get started with this project, clone the repository and install the dependencies: -```bash -git clone https://github.com/piyush-kacha/nestjs-starter-kit.git -cd nestjs-starter-kit +```shell +git clone https://github.com/piyush-kacha/nestjs-starter-kit.git YOUR_PROJECT_NAME # Replace YOUR_PROJECT_NAME with the name of your project +cd YOUR_PROJECT_NAME npm ci ``` +Use the copy command to create a copy of the example .env file. Replace `example.env` with the name of your example (`.env` or `.env.local` or `.env.development` or `.env.test`) file: -Use the copy command to create a copy of the example .env file. Replace `example.env` with the name of your example .env file: - -```bash -cp example.env .env +```shell +cp example.env .env # OR .env.local - .env.development - .env.test ``` ## Generating an RSA Key Pair for JWT Authentication with OpenSSL -1. Generate a private key: +1. Generate a private key: + + ```shell + openssl genrsa -out .keys/private_key.pem 2048 + ``` + +1. Extract the public key from the private key: + + ```shell + openssl rsa -in .keys/private_key.pem -outform PEM -pubout -out .keys/public_key.pem + ``` - ```sh - openssl genrsa -out private_key.pem 2048 - ``` -2. Extract the public key from the private key: - ```sh - openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem - ``` - This will create two files in the current directory: private_key.pem (the private key) and public_key.pem (the corresponding public key). + This will create two files in the `.keys` directory **(THIS DIRECTORY IS IGNORED)**: `.keys/private_key.pem` (the private key) and `.keys/public_key.pem` (the corresponding public key). **DON'T SHARE THE PRIVATE KEY WITH ANYONE.** - Note: The key size (2048 bits in this example) can be adjusted to suit your security requirements. -3. Encode the public key in base64 encoding: - ```sh - openssl base64 -A -in public_key.pem -out public_key_base64.txt - ``` - Copy the contents of the public_key_base64.txt file and paste it into the public_key_base64.txt file in .env JWT_PUBLIC_KEY variable. + **_NOTE:_** The key size (2048 bits in this example) can be adjusted to suit your security requirements. -4. Encode the private key in base64 encoding: - ```sh - openssl base64 -A -in private_key.pem -out private_key_base64.txt - ``` - Copy the contents of the private_key_base64.txt file and paste it into the private_key_base64.txt file in .env JWT_PRIVATE_KEY variable. +1. Encode the public key in base64 encoding: -5. Remove the public_key.pem and private_key.pem files. + ```shell + openssl base64 -A -in .keys/public_key.pem -out .keys/public_key_base64.txt + ``` -6. Remove the public_key_base64.txt and private_key_base64.txt files. + Copy the contents of the `.keys/public_key_base64.txt` file and paste it into the `JWT_PUBLIC_KEY` variable declared in your `.env` file. +1. Encode the private key in base64 encoding: + + ```sh + openssl base64 -A -in .keys/private_key.pem -out .keys/private_key_base64.txt + ``` + + Copy the contents of the `.keys/private_key_base64.txt` file and paste it into the `JWT_PRIVATE_KEY` variable declared in your `.env` file. ## Running the Application + To run the application in development mode, use the following command: + ```bash npm run start:dev ``` @@ -59,11 +63,13 @@ npm run start:dev This will start the application in watch mode, so any changes you make to the code will automatically restart the server. To run the application in production mode, use the following command: + ```bash npm run start:prod ``` -## Features: +## Features + 1. **Modularity**: The project structure follows a modular approach, organizing files into directories based on their functionalities. This makes it easier to locate and maintain code related to specific features or components of the application. 2. **Scalability**: The modular structure allows for easy scalability as the application grows. New modules, controllers, services, and other components can be added without cluttering the main directory. @@ -86,110 +92,165 @@ npm run start:prod By leveraging this code structure, you can benefit from the well-organized and maintainable foundation provided by the NestJS starter kit. It provides a solid structure for building scalable and robust applications while incorporating best practices and popular libraries. - -# Project Structure +## Project Structure ```bash nestjs-starter-kit/ -. -├── Dockerfile -├── LICENSE -├── README.md -├── .husky/ -│ ├── commit-msg -│ └── pre-commit -├── src -│   ├── app.config.ts -│   ├── app.controller.spec.ts -│   ├── app.controller.ts -│   ├── app.module.ts -│   ├── app.service.ts -│   ├── config -│   │   ├── database.config.ts -│   │   ├── index.ts -│   │   └── jwt.config.ts -│   ├── exceptions -│   │   ├── bad-request.exception.ts -│   │   ├── exceptions.constants.ts -│   │   ├── exceptions.interface.ts -│   │   ├── forbidden.exception.ts -│   │   ├── index.ts -│   │   ├── internal-server-error.exception.ts -│   │   └── unauthorized.exception.ts -│   ├── filters -│   │   ├── all-exception.filter.ts -│   │   ├── bad-request-exception.filter.ts -│   │   ├── forbidden-exception.filter.ts -│   │   ├── index.ts -│   │   ├── internal-server-error-exception.filter.ts -│   │   ├── not-found-exception.filter.ts -│   │   ├── unauthorized-exception.filter.ts -│   │   └── validator-exception.filter.ts -│   ├── main.ts -│   ├── modules -│   │   ├── auth -│   │   │   ├── auth.controller.ts -│   │   │   ├── auth.module.ts -│   │   │   ├── auth.service.ts -│   │   │   ├── decorators -│   │   │   │   └── get-user.decorator.ts -│   │   │   ├── dtos -│   │   │   │   ├── index.ts -│   │   │   │   ├── login.req.dto.ts -│   │   │   │   ├── login.res.dto.ts -│   │   │   │   ├── signup.req.dto.ts -│   │   │   │   └── signup.res.dto.ts -│   │   │   ├── guards -│   │   │   │   └── jwt-user-auth.guard.ts -│   │   │   ├── interfaces -│   │   │   │   └── jwt-user-payload.interface.ts -│   │   │   └── strategies -│   │   │   └── jwt-user.strategy.ts -│   │   ├── user -│   │   │   ├── dtos -│   │   │   │   ├── get-profile.res.dto.ts -│   │   │   │   └── index.ts -│   │   │   ├── user.controller.ts -│   │   │   ├── user.module.ts -│   │   │   ├── user.query.service.ts -│   │   │   ├── user.repository.ts -│   │   │   └── user.schema.ts -│   │   └── workspace -│   │   ├── workspace.module.ts -│   │   ├── workspace.query-service.ts -│   │   ├── workspace.repository.ts -│   │   └── workspace.schema.ts -│   └── shared -│   ├── enums -│   │   ├── db.enum.ts -│   │   ├── index.ts -│   │   ├── log-level.enum.ts -│   │   └── node-env.enum.ts -│   └── types -│   ├── index.ts -│   └── schema.type.ts -├── test -│   ├── app.e2e-spec.ts -│   └── jest-e2e.json -├── tsconfig.build.json -└── tsconfig.json -├── example.env -├── .commitlintrc.js -├── .dockerignore -├── .editorconfig -├── .eslintignore -├── .eslintrc.js -├── .gitignore -├── .lintstagedrc.js -├── .npmignore -├── .npmrc -├── .prettierignore -├── .prettierrc -├── nest-cli.json -├── package-lock.json -├── package.json -├── renovate.json +├─ .commitlintrc.js +├─ .dockerignore +├─ .editorconfig +├─ .eslintignore +├─ .eslintrc.js +├─ .gitignore +├─ .husky/ +│ ├─ _/ +│ │ ├─ .gitignore +│ │ └─ husky.sh +│ ├─ commit-msg +│ └─ pre-commit +├─ .keys/ +│ └─ .gitkeep +├─ .lintstagedrc.js +├─ .npmignore +├─ .npmrc +├─ .prettierignore +├─ .prettierrc +├─ .release-it.json +├─ .vscode/ +│ ├─ extensions.json +│ └─ settings.json +├─ Dockerfile +├─ LICENSE +├─ README.md +├─ example.env +├─ nest-cli.json +├─ package-lock.json +├─ package.json +├─ renovate.json +├─ src/ +│ ├─ app.module.ts +│ ├─ config/ +│ │ ├─ api.config.ts +│ │ ├─ app.config.ts +│ │ ├─ auth.config.ts +│ │ ├─ database.config.ts +│ │ ├─ infra.config.ts +│ │ └─ swagger.config.ts +│ ├─ core/ +│ │ ├─ application/ +│ │ │ ├─ application.controller.ts +│ │ │ ├─ application.module.ts +│ │ │ └─ application.service.ts +│ │ ├─ core.module.ts +│ │ ├─ database/ +│ │ │ ├─ abstracts/ +│ │ │ │ ├─ database.abstract.interface.ts +│ │ │ │ ├─ database.abstract.repository.ts +│ │ │ │ └─ index.ts +│ │ │ ├─ database-schema-options.ts +│ │ │ ├─ database.module.ts +│ │ │ └─ database.service.ts +│ │ ├─ exceptions/ +│ │ │ ├─ all.exception.ts +│ │ │ ├─ bad-request.exception.ts +│ │ │ ├─ constants/ +│ │ │ │ ├─ exceptions.constants.ts +│ │ │ │ └─ index.ts +│ │ │ ├─ forbidden.exception.ts +│ │ │ ├─ gateway-timeout.exception.ts +│ │ │ ├─ index.ts +│ │ │ ├─ interfaces/ +│ │ │ │ ├─ exceptions.interface.ts +│ │ │ │ └─ index.ts +│ │ │ ├─ internal-server-error.exception.ts +│ │ │ └─ unauthorized.exception.ts +│ │ ├─ filters/ +│ │ │ ├─ all-exception.filter.ts +│ │ │ ├─ bad-request-exception.filter.ts +│ │ │ ├─ forbidden-exception.filter.ts +│ │ │ ├─ gateway-timeout.exception.filter.ts +│ │ │ ├─ index.ts +│ │ │ ├─ internal-server-error-exception.filter.ts +│ │ │ ├─ not-found-exception.filter.ts +│ │ │ ├─ unauthorized-exception.filter.ts +│ │ │ └─ validator-exception.filter.ts +│ │ ├─ interceptors/ +│ │ │ ├─ index.ts +│ │ │ ├─ serializer.interceptor.ts +│ │ │ └─ timeout.interceptor.ts +│ │ ├─ log/ +│ │ │ └─ log.module.ts +│ │ └─ middlewares/ +│ │ ├─ index.ts +│ │ └─ logging.middleware.ts +│ ├─ main.ts +│ ├─ metadata.ts +│ ├─ modules/ +│ │ ├─ auth/ +│ │ │ ├─ auth.controller.ts +│ │ │ ├─ auth.module.ts +│ │ │ ├─ auth.service.ts +│ │ │ ├─ decorators/ +│ │ │ │ ├─ get-user.decorator.ts +│ │ │ │ ├─ index.ts +│ │ │ │ └─ public.decorator.ts +│ │ │ ├─ dtos/ +│ │ │ │ ├─ index.ts +│ │ │ │ ├─ login.req.dto.ts +│ │ │ │ ├─ login.res.dto.ts +│ │ │ │ ├─ signup.req.dto.ts +│ │ │ │ └─ signup.res.dto.ts +│ │ │ ├─ guards/ +│ │ │ │ ├─ index.ts +│ │ │ │ └─ jwt-user-auth.guard.ts +│ │ │ ├─ interfaces/ +│ │ │ │ ├─ index.ts +│ │ │ │ └─ jwt-user-payload.interface.ts +│ │ │ └─ strategies/ +│ │ │ ├─ index.ts +│ │ │ └─ jwt-user.strategy.ts +│ │ ├─ modules.module.ts +│ │ ├─ user/ +│ │ │ ├─ dtos/ +│ │ │ │ ├─ get-profile.res.dto.ts +│ │ │ │ └─ index.ts +│ │ │ ├─ user.controller.ts +│ │ │ ├─ user.module.ts +│ │ │ ├─ user.query.service.ts +│ │ │ ├─ user.repository.ts +│ │ │ └─ user.schema.ts +│ │ └─ workspace/ +│ │ ├─ workspace.module.ts +│ │ ├─ workspace.query-service.ts +│ │ ├─ workspace.repository.ts +│ │ └─ workspace.schema.ts +│ ├─ shared/ +│ │ ├─ decorators/ +│ │ │ ├─ api-error-responses.decorator.ts +│ │ │ └─ index.ts +│ │ ├─ enums/ +│ │ │ ├─ db.enum.ts +│ │ │ └─ index.ts +│ │ ├─ index.ts +│ │ └─ types/ +│ │ ├─ index.ts +│ │ ├─ or-never.type.ts +│ │ └─ schema.type.ts +│ └─ utils/ +│ ├─ date-time.util.ts +│ ├─ deep-resolver.util.ts +│ ├─ document-entity-helper.util.ts +│ ├─ index.ts +│ ├─ number.util.ts +│ ├─ validate-config.util.ts +│ └─ validation-options.util.ts +├─ test/ +│ ├─ app.e2e-spec.ts +│ └─ jest-e2e.json +├─ tsconfig.build.json +└─ tsconfig.json ``` + This project follows a structured organization to maintain clean, scalable code, promoting best practices for enterprise-level applications. ### 1. Root Files and Configuration @@ -201,24 +262,29 @@ This project follows a structured organization to maintain clean, scalable code, ### 2. Source Code (`src/`) -- **`app.config.ts`**: Centralizes application configuration settings. -- **`app.controller.ts`**: Defines the root controller for handling incoming requests. - **`app.module.ts`**: The main module that aggregates all the feature modules and services. -- **`app.service.ts`**: Contains the primary business logic for the application. - **`main.ts`**: The entry point of the NestJS application; bootstraps the application and configures clustering. #### Subdirectories within `src/` -- **`config/`**: Stores configuration files (e.g., `database.config.ts`, `jwt.config.ts`) for different aspects of the application. -- **`exceptions/`**: Custom exception classes (e.g., `bad-request.exception.ts`, `unauthorized.exception.ts`) that extend NestJS's built-in exceptions. -- **`filters/`**: Custom exception filters (e.g., `all-exception.filter.ts`, `not-found-exception.filter.ts`) for handling different types of errors globally. +- **`config/`**: Stores configuration files (e.g., `database.config.ts`, `auth.config.ts`) for different aspects of the application. +- **`core/`**: Core functionalities and shared resources: + - **`application/`**: Main application logic and services. + - **`database/`**: Database-related configurations and services. + - **`exceptions/`**: Custom exception classes. + - **`filters/`**: Custom exception filters. + - **`interceptors/`**: Interceptors for request/response handling. + - **`log/`**: Logging module. + - **`middlewares/`**: Custom middlewares. - **`modules/`**: Contains feature modules of the application: - - **`auth/`**: Handles authentication-related functionality, including controllers, services, guards, and strategies. - - **`user/`**: Manages user-related operations, including controllers, services, repositories, and schemas. - - **`workspace/`**: Manages workspace-related functionality, with services, repositories, and schemas. + - **`auth/`**: Handles authentication-related functionality. + - **`user/`**: Manages user-related operations. + - **`workspace/`**: Manages workspace-related functionality. - **`shared/`**: Contains shared resources and utilities: - - **`enums/`**: Defines enumerations (e.g., `db.enum.ts`, `node-env.enum.ts`) used across the application. - - **`types/`**: Custom TypeScript types (e.g., `schema.type.ts`) used for type safety throughout the codebase. + - **`decorators/`**: Custom decorators. + - **`enums/`**: Enumerations used across the application. + - **`types/`**: Custom TypeScript types. +- **`utils/`**: Utility functions for common operations. ### 3. Testing (`test/`) diff --git a/example.env b/example.env index 12698d4..d49aa86 100644 --- a/example.env +++ b/example.env @@ -1,32 +1,46 @@ -## Environment variables for the application - -# The environment the application is running in (e.g. development, production, test) -NODE_ENV= - -# The port number that the application listens on -PORT= - -# The hostname or IP address that the application binds to (e.g. localhost, 127.0.0.1) -HOST= - -# The logging level for the application (e.g. debug, info, warn, error) -LOG_LEVEL= - -# Whether to enable clustering mode for the application (true or false) -CLUSTERING= - -# The URI for the MongoDB database used by the application -MONGODB_URI= - -# The private key for generating JSON Web Tokens (JWTs) -# in README.md for more information how to generate a private key -JWT_PRIVATE_KEY= - -# The public key for verifying JSON Web Tokens (JWTs) -# in README.md for more information how to generate a public key -JWT_PUBLIC_KEY= - -# The expiration time for JSON Web Tokens (JWTs) -# expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). -# Eg: 60, "2 days", "10h", "7d" -JWT_EXPIRATION_TIME= \ No newline at end of file +# ========================================================================================= +# APPLICATION CONFIGURATION +# ========================================================================================= +NODE_ENV = "development" # The environment the application is running in (e.g. development, production, test) +PORT = 3000 # The port number that the application listens on +HOST = "localhost" # The hostname or IP address that the application binds to (e.g. localhost, 127.0.0.1) +LOG_LEVEL = "info" # The logging level for the application (e.g. debug, info, warn, error) + +# ========================================================================================= +# API CONFIGURATION +# ========================================================================================= +API_PREFIX_ENABLED = true # Enable or disable API prefix +API_PREFIX = "api" # The API prefix + +# ========================================================================================= +# INFRA CONFIGURATION +# ========================================================================================= +CLUSTERING = false # Whether to enable clustering mode for the application (true or false) +REQUEST_TIMEOUT = 30000 # (OPTIONAL) Reques timeout in ms. Default: 30 seconds + +# ========================================================================================= +# DATABASE CONFIGURATION +# ========================================================================================= +MONGODB_URI = "mongodb://localhost:27017/nestjs_starter_kit" # The URI for the MongoDB database used by the application +MONGODB_AUTO_CREATE = true # (OPTIONAL) Auto create collections. Default: true +MONGODB_AUTO_POPULATE = true # (OPTIONAL) Auto populate data. Default: true +MONGODB_HEARTBEAT_FREQUENCY_MS = 10000 # (OPTIONAL) Heartbeat frequency in ms. Default: 10000 + +# ========================================================================================= +# AUTHENTICATION CONFIGURATION +# ========================================================================================= +BCRYPT_SALT_ROUNDS = 10 # The salt rounds to hash passwords using bcrypt +JWT_PRIVATE_KEY = "" # The private key for generating JSON Web Tokens (JWTs) in README.md for more information how to generate a private key +JWT_PUBLIC_KEY = "" # The public key for verifying JSON Web Tokens (JWTs) in README.md for more information how to generate a public key +JWT_EXPIRATION_TIME = "" # The expiration time for JSON Web Tokens (JWTs) expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" + +# ========================================================================================= +# SWAGGER CONFIGURATION +# ========================================================================================= +SWAGGER_ENABLED = true # Enable swagger documentation +SWAGGER_TITLE = "NestJS Starter API" # (OPTIONAL) Swagger docs title +SWAGGER_DESCRIPTION = "The API for the NestJS Starter project" # (OPTIONAL) Swagger description +SWAGGER_VERSION = "1.0" # (OPTIONAL) Swagger version +SWAGGER_PATH = "docs" # (OPTIONAL) Swagger docs url path +SWAGGER_JSON_PATH = "docs/json" # (OPTIONAL) Swagger JSON url path +SWAGGER_YAML_PATH = "docs/yml" # (OPTIONAL) Swagger YAML url path diff --git a/nest-cli.json b/nest-cli.json index f9aa683..666fb58 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,22 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": true, + "builder": { + "type": "swc", + "options": { + "swcrcPath": ".swcrc" + } + }, + "typeCheck": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "classValidatorShim": false, + "introspectComments": true + } + } + ] } } diff --git a/package-lock.json b/package-lock.json index db8cd6d..3fcad18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "mongoose": "^8.6.0", + "mongoose-autopopulate": "^1.1.0", "nestjs-pino": "^4.0.0", "passport-jwt": "^4.0.1", "pino": "^9.0.0", @@ -36,6 +37,8 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", "@nestjs/testing": "^10.3.8", + "@swc/cli": "^0.3.14", + "@swc/core": "^1.7.26", "@types/express": "^4.17.21", "@types/jest": "29.5.12", "@types/node": "20.16.2", @@ -62,6 +65,9 @@ "ts-node": "^10.9.2", "tsconfig-paths": "4.2.0", "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1874,6 +1880,26 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", + "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", @@ -1882,6 +1908,311 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -2310,6 +2641,19 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -2328,6 +2672,323 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/cli": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.3.14.tgz", + "integrity": "sha512-0vGqD6FSW67PaZUZABkA+ADKsX7OUY/PwNEz1SbQdCvVk/e4Z36Gwh7mFVBQH9RIsMonTyhV1RHkwkGnEfR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mole-inc/bin-wrapper": "^8.0.1", + "@swc/counter": "^0.1.3", + "commander": "^8.3.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "piscina": "^4.3.0", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@swc/core": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2403,6 +3064,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2477,6 +3151,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -2538,6 +3219,16 @@ "@types/node": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2583,6 +3274,16 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.7", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", @@ -3219,6 +3920,27 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3566,6 +4288,183 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-check/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/bin-check/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-check/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3772,6 +4671,51 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -4093,6 +5037,19 @@ "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4471,6 +5428,35 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -4512,6 +5498,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -5438,6 +6434,19 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -5522,6 +6531,33 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -5649,6 +6685,24 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -5682,6 +6736,37 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", + "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5724,17 +6809,33 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "semver-regex": "^4.0.5" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6299,6 +7400,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6456,6 +7583,13 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6471,6 +7605,33 @@ "node": ">= 0.8" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6485,6 +7646,7 @@ "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true, + "license": "MIT", "bin": { "husky": "lib/bin.js" }, @@ -8618,6 +9780,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8810,6 +9982,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8947,6 +10129,15 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose-autopopulate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongoose-autopopulate/-/mongoose-autopopulate-1.1.0.tgz", + "integrity": "sha512-nTlTMlu1fLQ1bmJT7ILKbZmPGt2fHErLO4UJwzMDsHSigjtUYz0l3nvFhg511QkOkZcKBRzOnPn3DmmLIUENzg==", + "license": "Apache 2.0", + "peerDependencies": { + "mongoose": "6.x || 7.x || 8.0.0-rc0 || 8.x" + } + }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9121,6 +10312,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -9319,6 +10523,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -9328,6 +10545,26 @@ "node": ">=0.10.0" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9515,6 +10752,20 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", "peer": true }, + "node_modules/peek-readable": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.2.0.tgz", + "integrity": "sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -9545,6 +10796,16 @@ "node": ">=0.10" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pino": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.0.0.tgz", @@ -9741,6 +11002,16 @@ "node": ">= 6" } }, + "node_modules/piscina": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.7.0.tgz", + "integrity": "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -9924,6 +11195,13 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -10191,6 +11469,38 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -10302,6 +11612,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -10344,6 +11661,19 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -10566,6 +11896,35 @@ "node": ">=10" } }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10810,6 +12169,32 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -11084,6 +12469,16 @@ "node": ">=8" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -11117,6 +12512,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", + "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz", + "integrity": "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.1.3" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/superagent": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", @@ -11448,6 +12874,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -11477,6 +12921,32 @@ "node": ">=8" } }, + "node_modules/trim-repeated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", + "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", diff --git a/package.json b/package.json index 66b7da0..2046efa 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "mongoose": "^8.6.0", + "mongoose-autopopulate": "^1.1.0", "nestjs-pino": "^4.0.0", "passport-jwt": "^4.0.1", "pino": "^9.0.0", @@ -57,6 +58,8 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", "@nestjs/testing": "^10.3.8", + "@swc/cli": "^0.3.14", + "@swc/core": "^1.7.26", "@types/express": "^4.17.21", "@types/jest": "29.5.12", "@types/node": "20.16.2", @@ -83,5 +86,8 @@ "ts-node": "^10.9.2", "tsconfig-paths": "4.2.0", "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/src/app.config.ts b/src/app.config.ts deleted file mode 100644 index f40126c..0000000 --- a/src/app.config.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Import external modules -import * as crypto from 'crypto'; // Used to generate random UUIDs -import { IncomingMessage, ServerResponse } from 'http'; // Used to handle incoming and outgoing HTTP messages -import { Params } from 'nestjs-pino'; // Used to define parameters for the Pino logger - -// Import internal modules -import { LogLevel, NodeEnv } from './shared/enums'; // Import application enums - -export class AppConfig { - public static getLoggerConfig(): Params { - // Define the configuration for the Pino logger - const { NODE_ENV, LOG_LEVEL, CLUSTERING } = process.env; - - return { - exclude: [], // Exclude specific path from the logs and may not work for e2e testing - pinoHttp: { - genReqId: () => crypto.randomUUID(), // Generate a random UUID for each incoming request - autoLogging: true, // Automatically log HTTP requests and responses - base: CLUSTERING === 'true' ? { pid: process.pid } : {}, // Include the process ID in the logs if clustering is enabled - customAttributeKeys: { - responseTime: 'timeSpent', // Rename the responseTime attribute to timeSpent - }, - level: LOG_LEVEL || (NODE_ENV === NodeEnv.PRODUCTION ? LogLevel.INFO : LogLevel.TRACE), // Set the log level based on the environment and configuration - serializers: { - req(request: IncomingMessage) { - return { - method: request.method, - url: request.url, - id: request.id, - // Including the headers in the log could be in violation of privacy laws, e.g. GDPR. - // headers: request.headers, - }; - }, - res(reply: ServerResponse) { - return { - statusCode: reply.statusCode, - }; - }, - }, - transport: - NODE_ENV !== NodeEnv.PRODUCTION // Only use Pino-pretty in non-production environments - ? { - target: 'pino-pretty', - options: { - translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', - }, - } - : null, - }, - }; - } -} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index ccea57f..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index 0ce2d77..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiTags } from '@nestjs/swagger'; -import { Controller, Get } from '@nestjs/common'; - -import { AppService } from './app.service'; - -@ApiTags('Health-check') -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 13cdb3b..a08754e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,78 +1,97 @@ -// Import required modules -import { APP_FILTER, APP_PIPE } from '@nestjs/core'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { LoggerModule } from 'nestjs-pino'; -import { Module, ValidationError, ValidationPipe } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +/** + * AppModule is the main application module that combines CoreModule and ModulesModule. + * It provides various filters and interceptors for handling exceptions and requests. + */ +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -// Import application files - -import { AppConfig } from './app.config'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { configuration } from './config/index'; - -// Import filters +import { CoreModule } from './core/core.module'; import { AllExceptionsFilter, BadRequestExceptionFilter, ForbiddenExceptionFilter, + GatewayTimeOutExceptionFilter, NotFoundExceptionFilter, UnauthorizedExceptionFilter, ValidationExceptionFilter, -} from './filters'; -import { AuthModule } from './modules/auth/auth.module'; -import { UserModule } from './modules/user/user.module'; -import { WorkspaceModule } from './modules/workspace/workspace.module'; - -// Import other modules +} from './core/filters'; +import { TimeoutInterceptor } from './core/interceptors'; +import { ModulesModule } from './modules/modules.module'; +import { JwtUserAuthGuard } from './modules/auth/guards'; +/** + * The main application module for the NestJS starter kit. + * + * @module AppModule + * + * @imports + * - CoreModule: The core module of the application. + * - ModulesModule: The main modules of the application. + * + * @providers + * - APP_FILTER: + * - AllExceptionsFilter: Handles all exceptions. + * - BadRequestExceptionFilter: Handles bad request exceptions. + * - UnauthorizedExceptionFilter: Handles unauthorized exceptions. + * - ForbiddenExceptionFilter: Handles forbidden exceptions. + * - NotFoundExceptionFilter: Handles not found exceptions. + * - GatewayTimeOutExceptionFilter: Handles gateway timeout exceptions. + * - APP_PIPE: + * - ValidationExceptionFilter: Handles validation exceptions. + * - APP_INTERCEPTOR: + * - TimeoutInterceptor: Intercepts requests and applies a timeout based on configuration. + * - APP_GUARD: + * - JwtUserAuthGuard: Guards routes using JWT authentication. + * + * @param {ConfigService} configService - Service to manage application configuration. + * @returns {TimeoutInterceptor} - Returns a new instance of TimeoutInterceptor with the configured timeout. + */ @Module({ - imports: [ - // Configure environment variables - ConfigModule.forRoot({ - isGlobal: true, // Make the configuration global - load: [configuration], // Load the environment variables from the configuration file - }), - - // Configure logging - LoggerModule.forRoot(AppConfig.getLoggerConfig()), // ! forRootAsync is not working with ConfigService in nestjs-pino - - // Configure mongoose - MongooseModule.forRootAsync({ - imports: [ConfigModule], // Import the ConfigModule so that it can be injected into the factory function - inject: [ConfigService], // Inject the ConfigService into the factory function - useFactory: async (configService: ConfigService) => ({ - // Get the required configuration settings from the ConfigService - uri: configService.get('database.uri'), - }), - }), - // Import other modules - AuthModule, - UserModule, - WorkspaceModule, - ], - controllers: [AppController], // Define the application's controller + imports: [CoreModule, ModulesModule], providers: [ - AppService, - { provide: APP_FILTER, useClass: AllExceptionsFilter }, - { provide: APP_FILTER, useClass: ValidationExceptionFilter }, - { provide: APP_FILTER, useClass: BadRequestExceptionFilter }, - { provide: APP_FILTER, useClass: UnauthorizedExceptionFilter }, - { provide: APP_FILTER, useClass: ForbiddenExceptionFilter }, - { provide: APP_FILTER, useClass: NotFoundExceptionFilter }, { - // Allowing to do validation through DTO - // Since class-validator library default throw BadRequestException, here we use exceptionFactory to throw - // their internal exception so that filter can recognize it + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, + { provide: APP_PIPE, - useFactory: () => - new ValidationPipe({ - exceptionFactory: (errors: ValidationError[]) => { - return errors[0]; - }, - }), + useClass: ValidationExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: BadRequestExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: UnauthorizedExceptionFilter, }, - ], // Define the application's service + { + provide: APP_FILTER, + useClass: ForbiddenExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: NotFoundExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: GatewayTimeOutExceptionFilter, + }, + { + provide: APP_INTERCEPTOR, + useFactory: (configService: ConfigService) => { + const timeoutInMilliseconds = configService.get('infra.requestTimeout', { + infer: true, + }); + return new TimeoutInterceptor(timeoutInMilliseconds); + }, + inject: [ConfigService], + }, + { + provide: APP_GUARD, + useClass: JwtUserAuthGuard, + }, + ], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 48a63e3..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Yeah yeah! we are okay!'; - } -} diff --git a/src/config/api.config.ts b/src/config/api.config.ts new file mode 100644 index 0000000..e4ab654 --- /dev/null +++ b/src/config/api.config.ts @@ -0,0 +1,42 @@ +/** + * Validates environment variables configurations and emits a warning if any of them are incorrect. + */ +import { registerAs } from '@nestjs/config'; + +import { IsBoolean, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type ApiConfig = { + prefixEnabled: boolean; + prefix: string; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsBoolean() + API_PREFIX_ENABLED: boolean; + + @IsNotEmpty() + @IsString() + @MinLength(1) + API_PREFIX: string; +} + +/** + * Registers the API configuration settings. + * + * This configuration includes: + * - `prefixEnabled`: A boolean indicating if the API prefix is enabled, derived from the environment variable `API_PREFIX_ENABLED`. + * - `prefix`: A string representing the API prefix, derived from the environment variable `API_PREFIX` or defaults to 'api'. + * + * @returns {ApiConfig} The API configuration object. + */ +export default registerAs('api', (): ApiConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + + return { + prefixEnabled: process.env.API_PREFIX_ENABLED === 'true' ? true : false, + prefix: process.env.API_PREFIX || 'api', + }; +}); diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..b9a8011 --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,69 @@ +import { registerAs } from '@nestjs/config'; + +import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export enum E_APP_ENVIRONMENTS { + DEVELOPMENT = 'development', + PRODUCTION = 'production', + TEST = 'test', +} + +export enum E_APP_LOG_LEVELS { + SILENT = 'silent', + TRACE = 'trace', + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} + +export type AppConfig = { + env: string; + port: number; + host: string; + logLevel: string; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsEnum(E_APP_ENVIRONMENTS) + NODE_ENV: E_APP_ENVIRONMENTS; + + @IsInt() + @Min(1024) + @Max(49151) + PORT: number; + + @IsNotEmpty() + @IsString() + HOST: string; + + @IsNotEmpty() + @IsEnum(E_APP_LOG_LEVELS) + LOG_LEVEL: E_APP_LOG_LEVELS; +} + +/** + * Registers the application configuration using the `registerAs` function. + * Validates the environment variables using `validateConfigUtil` and `EnvironmentVariablesValidator`. + * + * @returns {AppConfig} The application configuration object. + * + * @property {string} env - The environment in which the application is running. Defaults to 'production'. + * @property {number} port - The port on which the application will run. Defaults to 3000. + * @property {string} host - The host address of the application. Defaults to 'localhost'. + * @property {string} logLevel - The logging level for the application. Defaults to 'info'. + */ +export default registerAs('app', (): AppConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + + return { + env: process.env.NODE_ENV || E_APP_ENVIRONMENTS.PRODUCTION, + port: parseInt(process.env.PORT, 10) || 3000, + host: process.env.HOST || 'localhost', + logLevel: process.env.LOG_LEVEL || 'info', + }; +}); diff --git a/src/config/auth.config.ts b/src/config/auth.config.ts new file mode 100644 index 0000000..cc83344 --- /dev/null +++ b/src/config/auth.config.ts @@ -0,0 +1,55 @@ +import { registerAs } from '@nestjs/config'; + +import { IsBase64, IsNotEmpty, IsNumber, IsPositive, IsString, Min } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type AuthenticationConfig = { + bcryptSaltRounds: number; + jwtPrivateKey: string; + jwtPublicKey: string; + jwtExpiresIn: string; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsNumber() + @IsPositive() + @Min(10) + BCRYPT_SALT_ROUNDS: number; + + @IsBase64() + @IsNotEmpty() + JWT_PRIVATE_KEY: string; + + @IsBase64() + @IsNotEmpty() + JWT_PUBLIC_KEY: string; + + @IsString() + @IsNotEmpty() + JWT_EXPIRATION_TIME: string; +} + +/** + * Registers the authentication configuration using the `registerAs` function. + * + * This configuration includes: + * - `bcryptSaltRounds`: The number of salt rounds to use for bcrypt hashing, parsed from the environment variable `BCRYPT_SALT_ROUNDS`. + * - `jwtPrivateKey`: The private key for JWT, decoded from base64 and converted to a UTF-8 string from the environment variable `JWT_PRIVATE_KEY`. + * - `jwtPublicKey`: The public key for JWT, decoded from base64 and converted to a UTF-8 string from the environment variable `JWT_PUBLIC_KEY`. + * - `jwtExpiresIn`: The expiration time for JWT tokens, taken from the environment variable `JWT_EXPIRATION_TIME` or defaulting to '1h'. + * + * Before returning the configuration, it validates the environment variables using `validateConfigUtil` and `EnvironmentVariablesValidator`. + * + * @returns {AuthenticationConfig} The authentication configuration object. + */ +export default registerAs('authentication', (): AuthenticationConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + return { + bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS, 10), + jwtPrivateKey: Buffer.from(process.env.JWT_PRIVATE_KEY, 'base64').toString('utf-8'), + jwtPublicKey: Buffer.from(process.env.JWT_PUBLIC_KEY, 'base64').toString('utf-8'), + jwtExpiresIn: process.env.JWT_EXPIRATION_TIME || '1h', + }; +}); diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 02a7a86..3b9ab4a 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,7 +1,53 @@ -export interface IDatabaseConfig { +import { registerAs } from '@nestjs/config'; + +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsPositive, IsString, Min } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type DatabaseConfig = { uri: string; + autoCreate: boolean; + autoPopulate: boolean; + heartbeatFrequencyMS: number; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsString() + MONGODB_URI: string; + + @IsOptional() + @IsBoolean() + MONGODB_AUTO_CREATE: boolean; + + @IsOptional() + @IsBoolean() + MONGODB_AUTO_POPULATE: boolean; + + @IsOptional() + @IsNumber() + @IsPositive() + @Min(10000) + MONGODB_HEARTBEAT_FREQUENCY_MS: number; } -export const databaseConfig = (): IDatabaseConfig => ({ - uri: process.env.MONGODB_URI, +/** + * Registers the database configuration using the `registerAs` function. + * Validates the environment variables using `validateConfigUtil` and `EnvironmentVariablesValidator`. + * + * @returns {DatabaseConfig} The database configuration object. + * @property {string} uri - The MongoDB URI from the environment variable `MONGODB_URI`. + * @property {boolean} autoCreate - Determines if auto creation is enabled, based on the environment variable `MONGODB_AUTO_CREATE`. + * @property {boolean} autoPopulate - Determines if auto population is enabled, based on the environment variable `MONGODB_AUTO_POPULATE`. + * @property {number} heartbeatFrequencyMS - The heartbeat frequency in milliseconds, based on the environment variable `MONGODB_HEARTBEAT_FREQUENCY_MS` or defaults to 10000. + */ +export default registerAs('database', (): DatabaseConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + + return { + uri: process.env.MONGODB_URI, + autoCreate: process.env.MONGODB_AUTO_CREATE && process.env.MONGODB_AUTO_CREATE === 'false' ? false : true, + autoPopulate: process.env.MONGODB_AUTO_POPULATE && process.env.MONGODB_AUTO_POPULATE === 'false' ? false : true, + heartbeatFrequencyMS: parseInt(process.env.MONGODB_HEARTBEAT_FREQUENCY_MS, 10) || 10000, + }; }); diff --git a/src/config/index.ts b/src/config/index.ts deleted file mode 100644 index 4758a73..0000000 --- a/src/config/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IDatabaseConfig, databaseConfig } from './database.config'; -import { IJwtConfig, jwtConfig } from './jwt.config'; -import { NodeEnv } from '../shared/enums/node-env.enum'; - -export interface IConfig { - env: string; - port: number; - host: string; - logLevel: string; - clustering: string; - database: IDatabaseConfig; - jwt: IJwtConfig; -} - -export const configuration = (): Partial => ({ - env: process.env.NODE_ENV || NodeEnv.DEVELOPMENT, - port: parseInt(process.env.PORT, 10) || 3009, - host: process.env.HOST || '127.0.0.1', - logLevel: process.env.LOG_LEVEL, - clustering: process.env.CLUSTERING, - database: databaseConfig(), - jwt: jwtConfig(), -}); diff --git a/src/config/infra.config.ts b/src/config/infra.config.ts new file mode 100644 index 0000000..cf81783 --- /dev/null +++ b/src/config/infra.config.ts @@ -0,0 +1,43 @@ +import { registerAs } from '@nestjs/config'; + +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsPositive } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type InfraConfig = { + clusteringEnabled: boolean; + requestTimeout: number; +}; + +class EnvironmentVariablesValidator { + @IsBoolean() + @IsNotEmpty() + CLUSTERING: boolean; + + @IsOptional() + @IsNumber() + @IsPositive() + REQUEST_TIMEOUT: number; +} + +/** + * Registers the infrastructure configuration for the application. + * + * @returns {InfraConfig} The infrastructure configuration object. + * + * @remarks + * This function uses the `registerAs` utility to register the configuration + * under the 'infra' namespace. It validates the environment variables using + * the `validateConfigUtil` function and the `EnvironmentVariablesValidator`. + * + * The configuration includes: + * - `clusteringEnabled`: A boolean indicating if clustering is enabled, based on the `CLUSTERING` environment variable. + * - `requestTimeout`: The request timeout duration in milliseconds, based on the `REQUEST_TIMEOUT` environment variable, defaulting to 30 seconds if not provided. + */ +export default registerAs('infra', (): InfraConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + return { + clusteringEnabled: process.env.CLUSTERING && process.env.CLUSTERING === 'true' ? true : false, + requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 30000, // 30 seconds + }; +}); diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts deleted file mode 100644 index 92b5f6f..0000000 --- a/src/config/jwt.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface IJwtConfig { - privateKey: string; - publicKey: string; - expiresIn: string; -} - -export const jwtConfig = (): IJwtConfig => ({ - privateKey: Buffer.from(process.env.JWT_PRIVATE_KEY, 'base64').toString('utf-8'), - publicKey: Buffer.from(process.env.JWT_PUBLIC_KEY, 'base64').toString('utf-8'), - expiresIn: process.env.JWT_EXPIRATION_TIME || '1h', -}); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..c333cc2 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,72 @@ +import { registerAs } from '@nestjs/config'; + +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type SwaggerConfig = { + swaggerEnabled: boolean; + swaggerTitle: string; + swaggerDescription: string; + swaggerVersion: string; + swaggerPath: string; + swaggerJsonPath: string; + swaggerYamlPath: string; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsBoolean() + SWAGGER_ENABLED: boolean; + + @IsOptional() + @IsString() + SWAGGER_TITLE: string; + + @IsOptional() + @IsString() + SWAGGER_DESCRIPTION: string; + + @IsOptional() + @IsString() + SWAGGER_VERSION: string; + + @IsOptional() + @IsString() + SWAGGER_PATH: string; + + @IsOptional() + @IsString() + SWAGGER_JSON_PATH: string; + + @IsOptional() + @IsString() + SWAGGER_YAML_PATH: string; +} + +/** + * Registers the Swagger configuration for the application. + * + * @returns {SwaggerConfig} The Swagger configuration object. + * + * The configuration includes: + * - `swaggerEnabled`: A boolean indicating if Swagger is enabled (default: true). + * - `swaggerTitle`: The title of the Swagger documentation (default: 'NestJS Starter API'). + * - `swaggerDescription`: The description of the Swagger documentation (default: 'The API for the NestJS Starter project'). + * - `swaggerVersion`: The version of the Swagger documentation (default: '1.0'). + * - `swaggerPath`: The path to access the Swagger UI (default: 'docs'). + * - `swaggerJsonPath`: The path to access the Swagger JSON documentation (default: 'docs/json'). + * - `swaggerYamlPath`: The path to access the Swagger YAML documentation (default: 'docs/yaml'). + */ +export default registerAs('swagger', (): SwaggerConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + return { + swaggerEnabled: process.env.SWAGGER_ENABLED && process.env.SWAGGER_ENABLED === 'false' ? false : true, + swaggerTitle: process.env.SWAGGER_TITLE || 'NestJS Starter API', + swaggerDescription: process.env.SWAGGER_DESCRIPTION || 'The API for the NestJS Starter project', + swaggerVersion: process.env.SWAGGER_VERSION || '1.0', + swaggerPath: process.env.SWAGGER_PATH || 'docs', + swaggerJsonPath: process.env.SWAGGER_JSON_PATH || 'docs/json', + swaggerYamlPath: process.env.SWAGGER_YAML_PATH || 'docs/yaml', + }; +}); diff --git a/src/core/application/application.controller.ts b/src/core/application/application.controller.ts new file mode 100644 index 0000000..0193c51 --- /dev/null +++ b/src/core/application/application.controller.ts @@ -0,0 +1,35 @@ +import { ApiTags } from '@nestjs/swagger'; +import { Controller, Get } from '@nestjs/common'; + +import { Public } from 'src/modules/auth/decorators'; + +import { ApplicationService } from './application.service'; + +/** + * Controller responsible for handling application-level routes. + * + * @remarks + * This controller provides a health check endpoint. + * + * @example + * ```typescript + * @Get() + * @Public() + * getHello(): string { + * return this.applicationService.getHello(); + * } + * ``` + * + * @public + */ +@ApiTags('Health Check') +@Controller() +export class ApplicationController { + constructor(private readonly applicationService: ApplicationService) {} + + @Get() + @Public() + getHello(): string { + return this.applicationService.getHello(); + } +} diff --git a/src/core/application/application.module.ts b/src/core/application/application.module.ts new file mode 100644 index 0000000..b0333b6 --- /dev/null +++ b/src/core/application/application.module.ts @@ -0,0 +1,67 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import apiConfig from 'src/config/api.config'; +import appConfig from 'src/config/app.config'; +import authConfig from 'src/config/auth.config'; +import databaseConfig from 'src/config/database.config'; +import infraConfig from 'src/config/infra.config'; +import swaggerConfig from 'src/config/swagger.config'; + +import { DatabaseModule } from '../database/database.module'; +import { LogModule } from '../log/log.module'; + +import { ApplicationController } from './application.controller'; +import { ApplicationService } from './application.service'; + +/** + * ApplicationModule is the root module of the application. + * It imports and configures various modules and services required for the application to function. + * + * @module ApplicationModule + * + * @description + * This module imports the following modules: + * - ConfigModule: Configures environment variables and loads configuration files. + * - LogModule: Handles logging functionality. + * - DatabaseModule: Manages database connections and operations. + * + * @imports + * - ConfigModule.forRoot: Configures environment variables with options for caching, file paths, variable expansion, and validation. + * - LogModule: Provides logging capabilities. + * - DatabaseModule: Manages database interactions. + * + * @controllers + * - ApplicationController: Handles incoming requests and routes them to appropriate services. + * + * @providers + * - ApplicationService: Contains business logic and application services. + * + * @exports + * - None + */ +@Module({ + imports: [ + // Configure environment variables + ConfigModule.forRoot({ + isGlobal: true, // Make the configuration global + cache: true, // Enable caching of the configuration + envFilePath: ['.env.local', '.env.development', '.env.test', '.env'], // Load the .env file + expandVariables: true, // Expand variables in the configuration + load: [apiConfig, appConfig, authConfig, databaseConfig, infraConfig, swaggerConfig], // Load the environment variables from the configuration file + validationOptions: { + // Validate the configuration + allowUnknown: true, // Allow unknown properties + abortEarly: true, // Abort on the first error + }, + }), + // Import the LogModule + LogModule, + // Import the DatabaseModule + DatabaseModule, + ], + controllers: [ApplicationController], + providers: [ApplicationService], + exports: [], +}) +export class ApplicationModule {} diff --git a/src/core/application/application.service.ts b/src/core/application/application.service.ts new file mode 100644 index 0000000..d5cb296 --- /dev/null +++ b/src/core/application/application.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; + +/** + * Service that provides application-level functionalities. + */ +@Injectable() +export class ApplicationService { + /** + * Returns a greeting message. + * + * @returns A string containing a greeting message. + */ + getHello(): string { + return 'Yeah yeah! we are okay! :)'; + } +} diff --git a/src/core/core.module.ts b/src/core/core.module.ts new file mode 100644 index 0000000..831cbff --- /dev/null +++ b/src/core/core.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationModule } from './application/application.module'; + +/** + * The CoreModule is a fundamental module in the NestJS application. + * It imports the ApplicationModule and serves as a central module + * for the core functionalities of the application. + * + * @module CoreModule + */ +@Module({ + imports: [ApplicationModule], + controllers: [], + providers: [], + exports: [], +}) +export class CoreModule {} diff --git a/src/core/database/abstracts/database.abstract.interface.ts b/src/core/database/abstracts/database.abstract.interface.ts new file mode 100644 index 0000000..d7d5a92 --- /dev/null +++ b/src/core/database/abstracts/database.abstract.interface.ts @@ -0,0 +1,12 @@ +/** + * Interface representing the abstract schema for a database entity. + * + * @property _id - Optional unique identifier for the entity. + * @property createdAt - Optional timestamp indicating when the entity was created. + * @property updatedAt - Optional timestamp indicating when the entity was last updated. + */ +export interface IDatabaseAbstractSchema { + _id?: string; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/src/core/database/abstracts/database.abstract.repository.ts b/src/core/database/abstracts/database.abstract.repository.ts new file mode 100644 index 0000000..b83d4e0 --- /dev/null +++ b/src/core/database/abstracts/database.abstract.repository.ts @@ -0,0 +1,80 @@ +import { Logger, NotFoundException } from '@nestjs/common'; + +import { Model } from 'mongoose'; + +/** + * Abstract class representing a generic database repository. + * Provides common CRUD operations for database documents. + * + * @template T - The type of the document. + */ +export abstract class DatabaseAbstractRepository { + /** + * Logger instance for logging repository actions. + */ + private readonly log: Logger; + + /** + * Creates an instance of DatabaseAbstractRepository. + * + * @param model - The Mongoose model representing the document. + */ + constructor(private readonly model: Model) { + this.log = new Logger(this.constructor.name); + } + + /** + * Retrieves all documents from the database. + * + * @returns A promise that resolves to an array of documents. + */ + async findAllDocuments(): Promise { + return this.model.find().exec(); + } + + /** + * Retrieves a single document by its ID. + * + * @param id - The ID of the document to retrieve. + * @returns A promise that resolves to the document if found. + * @throws NotFoundException if the document with the given ID is not found. + */ + async findOneDocumentById(id: string): Promise { + const document = await this.model.findById(id).exec(); + if (!document) { + throw new NotFoundException(`Document with ID ${id} not found`); + } + return document; + } + + /** + * Updates a document by its ID with the provided data. + * + * @param id - The ID of the document to update. + * @param data - The partial data to update the document with. + * @returns A promise that resolves to the updated document. + * @throws NotFoundException if the document with the given ID is not found. + */ + async updateDocumentById(id: string, data: Partial): Promise { + const updatedDocument = await this.model.findByIdAndUpdate(id, data, { new: true, useFindAndModify: false }).exec(); + if (!updatedDocument) { + throw new NotFoundException(`Document with ID ${id} not found`); + } + return updatedDocument; + } + + /** + * Deletes a document by its ID. + * + * @param id - The ID of the document to delete. + * @returns A promise that resolves to the deleted document. + * @throws NotFoundException if the document with the given ID is not found. + */ + async deleteDocumentById(id: string): Promise { + const deletedDocument = await this.model.findByIdAndDelete(id).exec(); + if (!deletedDocument) { + throw new NotFoundException(`Document with ID ${id} not found`); + } + return deletedDocument; + } +} diff --git a/src/core/database/abstracts/index.ts b/src/core/database/abstracts/index.ts new file mode 100644 index 0000000..6f5b035 --- /dev/null +++ b/src/core/database/abstracts/index.ts @@ -0,0 +1,2 @@ +export * from './database.abstract.interface'; +export * from './database.abstract.repository'; diff --git a/src/core/database/database-schema-options.ts b/src/core/database/database-schema-options.ts new file mode 100644 index 0000000..920b793 --- /dev/null +++ b/src/core/database/database-schema-options.ts @@ -0,0 +1,57 @@ +import { SchemaOptions } from '@nestjs/mongoose'; + +/** + * Generates schema options for a Mongoose schema. + * + * @param collectionName - The name of the collection. + * @param deleteProperties - Optional array of property names to delete from the JSON and object representations. + * @returns The schema options object. + * + * @remarks + * The returned schema options include: + * - `timestamps`: Automatically adds `createdAt` and `updatedAt` fields. + * - `collection`: Customizes the name of the collection. + * - `minimize`: Disables the removal of empty objects. + * - `versionKey`: Disables the `__v` version key. + * - `toJSON` and `toObject`: Customizes the transformation of the document to JSON and plain objects, respectively. + * - Removes `id`, `createdAt`, `updatedAt`, and `__v` fields. + * - Removes additional properties specified in `deleteProperties`. + */ +export function getDatabaseSchemaOptions(collectionName: string, deleteProperties?: string[]): SchemaOptions { + return { + timestamps: true, + collection: collectionName, // Permite personalizar el nombre de la colección + minimize: false, + versionKey: false, + toJSON: { + virtuals: true, + getters: true, + transform: (_doc, ret) => { + delete ret.id; + delete ret.createdAt; + delete ret.updatedAt; + delete ret.__v; + if (deleteProperties && deleteProperties.length > 0) { + deleteProperties.forEach((property) => { + delete ret[property]; + }); + } + }, + }, + toObject: { + virtuals: true, + getters: true, + transform: (_doc, ret) => { + delete ret.id; + delete ret.createdAt; + delete ret.updatedAt; + delete ret.__v; + if (deleteProperties && deleteProperties.length > 0) { + deleteProperties.forEach((property) => { + delete ret[property]; + }); + } + }, + }, + }; +} diff --git a/src/core/database/database.module.ts b/src/core/database/database.module.ts new file mode 100644 index 0000000..246d58e --- /dev/null +++ b/src/core/database/database.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { ConfigService } from '@nestjs/config'; + +import { DatabaseService } from './database.service'; + +/** + * The `DatabaseModule` is responsible for configuring and providing database-related services. + * + * @module + * + * @description + * This module imports the `MongooseModule` and configures it asynchronously using the `DatabaseService` class. + * The `ConfigService` is injected to provide necessary configuration for the database connection. + * + * @imports + * - `MongooseModule`: A module that provides MongoDB support via Mongoose. + * + * @injects + * - `ConfigService`: A service that provides configuration values. + * + * @useClass + * - `DatabaseService`: A service class that contains the logic for configuring the database connection. + */ +@Module({ + imports: [ + MongooseModule.forRootAsync({ + inject: [ConfigService], + useClass: DatabaseService, + }), + ], + providers: [], +}) +export class DatabaseModule {} diff --git a/src/core/database/database.service.ts b/src/core/database/database.service.ts new file mode 100644 index 0000000..27002b5 --- /dev/null +++ b/src/core/database/database.service.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MongooseModuleOptions, MongooseOptionsFactory } from '@nestjs/mongoose'; + +import { Connection } from 'mongoose'; + +/** + * Service responsible for configuring and providing Mongoose options. + * Implements the `MongooseOptionsFactory` interface to create Mongoose options. + * + * @class + * @implements {MongooseOptionsFactory} + */ +@Injectable() +export class DatabaseService implements MongooseOptionsFactory { + /** + * Logger instance for logging messages related to the DatabaseService. + * @private + * @readonly + * @type {Logger} + */ + private readonly logger: Logger = new Logger(DatabaseService.name); + + /** + * Creates an instance of DatabaseService. + * @param {ConfigService} configService - The configuration service to retrieve database settings. + */ + constructor(private readonly configService: ConfigService) {} + + /** + * Creates and returns Mongoose module options. + * Logs the process of creating Mongoose options and retrieves settings from the configuration service. + * Optionally enables the mongoose-autopopulate plugin if auto-populate is enabled in the configuration. + * + * @returns {MongooseModuleOptions} The Mongoose module options. + */ + createMongooseOptions(): MongooseModuleOptions { + const autoPopulate: boolean = this.configService.get('database.autoPopulate', { + infer: true, + }); + return { + uri: this.configService.get('database.uri', { + infer: true, + }), + autoCreate: this.configService.get('database.autoCreate', { + infer: true, + }), + heartbeatFrequencyMS: this.configService.get('database.heartbeatFrequencyMS', { + infer: true, + }), + onConnectionCreate(connection: Connection) { + if (autoPopulate) { + connection.plugin(require('mongoose-autopopulate')); // Enable the mongoose-autopopulate plugin + } + }, + }; + } +} diff --git a/src/core/exceptions/all.exception.ts b/src/core/exceptions/all.exception.ts new file mode 100644 index 0000000..9035b49 --- /dev/null +++ b/src/core/exceptions/all.exception.ts @@ -0,0 +1,136 @@ +/** + * A custom exception that represents a All error. + */ + +// Import required modules +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { HttpException, HttpStatus } from '@nestjs/common'; + +import { ExceptionConstants } from './constants'; +import { IException, IHttpAllExceptionResponse } from './interfaces'; + +/** + * Represents a custom exception that extends the HttpException class. + * This class is used to handle all types of exceptions in a standardized way. + */ +export class AllException extends HttpException { + /** + * A unique code identifying the error. + */ + @ApiProperty({ + enum: ExceptionConstants.InternalServerErrorCodes, + description: 'A unique code identifying the error.', + example: ExceptionConstants.InternalServerErrorCodes.UNEXPECTED_ERROR, + }) + code: number; // Internal status code + + /** + * Message for the exception. + */ + @ApiProperty({ + description: 'Message for the exception', + example: 'Internal Server Error', + }) + message: string; // Message for the exception + + /** + * A description of the error message. + */ + @ApiProperty({ + description: 'A description of the error message.', + example: 'An unexpected error occurred', + }) + description: string; // Description of the exception + + /** + * The cause of the exception. + */ + @ApiPropertyOptional({ + description: 'The cause of the exception', + example: {}, + }) + error?: unknown; // Error object + + /** + * Timestamp of the exception. + */ + @ApiProperty({ + description: 'Timestamp of the exception', + format: 'date-time', + example: '2022-12-31T23:59:59.999Z', + }) + timestamp: string; // Timestamp of the exception + + /** + * Trace ID of the request. + */ + @ApiProperty({ + description: 'Trace ID of the request', + example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', + }) + traceId: string; // Trace ID of the request + + /** + * Constructs a new AllException object. + * @param exception An object containing the exception details. + * - message: A string representing the error message. + * - cause: An object representing the cause of the error. + * - description: A string describing the error in detail. + * - code: A number representing internal status code which helpful in future for frontend + */ + constructor(exception: IException) { + super(exception.message, HttpStatus.INTERNAL_SERVER_ERROR, { + cause: exception.cause, + description: exception.description, + }); + + this.code = exception.code; + this.message = exception.message; + this.description = exception.description; + this.timestamp = new Date().toISOString(); + } + + /** + * Set the Trace ID of the AllException instance. + * @param traceId A string representing the Trace ID. + */ + setTraceId = (traceId: string) => { + this.traceId = traceId; + }; + + /** + * Set the error of the AllException instance. + * @param error An object representing the error. + */ + setError = (error: unknown) => { + this.error = error; + }; + + /** + * Generate an HTTP response body representing the AllException instance. + * @param message A string representing the message to include in the response body. + * @returns An object representing the HTTP response body. + */ + generateHttpResponseBody = (message?: string): IHttpAllExceptionResponse => { + return { + code: this.code, + message: message || this.message, + description: this.description, + error: this.error, + timestamp: this.timestamp, + traceId: this.traceId, + }; + }; + + /** + * Returns a new instance of BadRequestException representing an Unexpected Error. + * @param msg A string representing the error message. + * @returns An instance of BadRequestException representing the error. + */ + static UNEXPECTED = (msg?: string) => { + return new AllException({ + message: msg || 'Unexpected Error', + code: ExceptionConstants.InternalServerErrorCodes.UNEXPECTED_ERROR, + }); + }; +} diff --git a/src/exceptions/bad-request.exception.ts b/src/core/exceptions/bad-request.exception.ts similarity index 52% rename from src/exceptions/bad-request.exception.ts rename to src/core/exceptions/bad-request.exception.ts index a505a27..7912d78 100644 --- a/src/exceptions/bad-request.exception.ts +++ b/src/core/exceptions/bad-request.exception.ts @@ -1,59 +1,92 @@ -/** - * A custom exception that represents a BadRequest error. - */ - -// Import required modules import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { HttpException, HttpStatus } from '@nestjs/common'; -// Import internal modules -import { ExceptionConstants } from './exceptions.constants'; -import { IException, IHttpBadRequestExceptionResponse } from './exceptions.interface'; +import { ExceptionConstants } from './constants'; +import { IException, IHttpBadRequestExceptionResponse } from './interfaces'; +/** + * Represents a BadRequestException which extends the HttpException. + * This exception is used to handle bad request errors with detailed information. + * + * @class + * @extends HttpException + * + * @property {number} code - A unique code identifying the error. + * @property {Error} cause - The underlying cause of the exception. + * @property {string} message - Message for the exception. + * @property {string} description - A description of the error message. + * @property {string} timestamp - Timestamp of the exception in ISO format. + * @property {string} traceId - Trace ID of the request. + * + * @constructor + * @param {IException} exception - The exception object containing error details. + * + * @method setTraceId + * @param {string} traceId - Sets the trace ID of the request. + * + * @method generateHttpResponseBody + * @param {string} [message] - Optional custom message for the response body. + * @returns {IHttpBadRequestExceptionResponse} - The HTTP response body for the bad request exception. + * + * @static HTTP_REQUEST_TIMEOUT + * @returns {BadRequestException} - Returns a BadRequestException for HTTP request timeout. + * + * @static RESOURCE_ALREADY_EXISTS + * @param {string} [msg] - Optional custom message. + * @returns {BadRequestException} - Returns a BadRequestException for resource already exists. + * + * @static RESOURCE_NOT_FOUND + * @param {string} [msg] - Optional custom message. + * @returns {BadRequestException} - Returns a BadRequestException for resource not found. + * + * @static VALIDATION_ERROR + * @param {string} [msg] - Optional custom message. + * @returns {BadRequestException} - Returns a BadRequestException for validation error. + * + * @static UNEXPECTED + * @param {string} [msg] - Optional custom message. + * @returns {BadRequestException} - Returns a BadRequestException for unexpected error. + * + * @static INVALID_INPUT + * @param {string} [msg] - Optional custom message. + * @returns {BadRequestException} - Returns a BadRequestException for invalid input. + */ export class BadRequestException extends HttpException { @ApiProperty({ enum: ExceptionConstants.BadRequestCodes, description: 'A unique code identifying the error.', example: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, }) - code: number; // Internal status code + code: number; @ApiHideProperty() - cause: Error; // Error object causing the exception + cause: Error; @ApiProperty({ description: 'Message for the exception', example: 'Bad Request', }) - message: string; // Message for the exception + message: string; @ApiProperty({ description: 'A description of the error message.', example: 'The input provided was invalid', }) - description: string; // Description of the exception + description: string; @ApiProperty({ description: 'Timestamp of the exception', format: 'date-time', example: '2022-12-31T23:59:59.999Z', }) - timestamp: string; // Timestamp of the exception + timestamp: string; @ApiProperty({ description: 'Trace ID of the request', example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', }) - traceId: string; // Trace ID of the request - - /** - * Constructs a new BadRequestException object. - * @param exception An object containing the exception details. - * - message: A string representing the error message. - * - cause: An object representing the cause of the error. - * - description: A string describing the error in detail. - * - code: A number representing internal status code which helpful in future for frontend - */ + traceId: string; + constructor(exception: IException) { super(exception.message, HttpStatus.BAD_REQUEST, { cause: exception.cause, @@ -66,19 +99,10 @@ export class BadRequestException extends HttpException { this.timestamp = new Date().toISOString(); } - /** - * Set the Trace ID of the BadRequestException instance. - * @param traceId A string representing the Trace ID. - */ setTraceId = (traceId: string) => { this.traceId = traceId; }; - /** - * Generate an HTTP response body representing the BadRequestException instance. - * @param message A string representing the message to include in the response body. - * @returns An object representing the HTTP response body. - */ generateHttpResponseBody = (message?: string): IHttpBadRequestExceptionResponse => { return { code: this.code, @@ -89,10 +113,6 @@ export class BadRequestException extends HttpException { }; }; - /** - * Returns a new instance of BadRequestException representing an HTTP Request Timeout error. - * @returns An instance of BadRequestException representing the error. - */ static HTTP_REQUEST_TIMEOUT = () => { return new BadRequestException({ message: 'HTTP Request Timeout', @@ -100,11 +120,6 @@ export class BadRequestException extends HttpException { }); }; - /** - * Create a BadRequestException for when a resource already exists. - * @param {string} [msg] - Optional message for the exception. - * @returns {BadRequestException} - A BadRequestException with the appropriate error code and message. - */ static RESOURCE_ALREADY_EXISTS = (msg?: string) => { return new BadRequestException({ message: msg || 'Resource Already Exists', @@ -112,11 +127,6 @@ export class BadRequestException extends HttpException { }); }; - /** - * Create a BadRequestException for when a resource is not found. - * @param {string} [msg] - Optional message for the exception. - * @returns {BadRequestException} - A BadRequestException with the appropriate error code and message. - */ static RESOURCE_NOT_FOUND = (msg?: string) => { return new BadRequestException({ message: msg || 'Resource Not Found', @@ -124,11 +134,6 @@ export class BadRequestException extends HttpException { }); }; - /** - * Returns a new instance of BadRequestException representing a Validation Error. - * @param msg A string representing the error message. - * @returns An instance of BadRequestException representing the error. - */ static VALIDATION_ERROR = (msg?: string) => { return new BadRequestException({ message: msg || 'Validation Error', @@ -136,11 +141,6 @@ export class BadRequestException extends HttpException { }); }; - /** - * Returns a new instance of BadRequestException representing an Unexpected Error. - * @param msg A string representing the error message. - * @returns An instance of BadRequestException representing the error. - */ static UNEXPECTED = (msg?: string) => { return new BadRequestException({ message: msg || 'Unexpected Error', @@ -148,11 +148,6 @@ export class BadRequestException extends HttpException { }); }; - /** - * Returns a new instance of BadRequestException representing an Invalid Input. - * @param msg A string representing the error message. - * @returns An instance of BadRequestException representing the error. - */ static INVALID_INPUT = (msg?: string) => { return new BadRequestException({ message: msg || 'Invalid Input', diff --git a/src/core/exceptions/constants/exceptions.constants.ts b/src/core/exceptions/constants/exceptions.constants.ts new file mode 100644 index 0000000..85fcce6 --- /dev/null +++ b/src/core/exceptions/constants/exceptions.constants.ts @@ -0,0 +1,127 @@ +/** + * A class containing constants for various HTTP error codes. + * + * @class ExceptionConstants + * + * @property {Object} NotFoundCodes - Constants for not found HTTP error codes. + * @property {number} NotFoundCodes.URL_NOT_FOUND - The requested URL was not found on the server. + * @property {number} NotFoundCodes.RESOURCE_NOT_FOUND - The requested resource could not be found. + * @property {number} NotFoundCodes.RESOURCE_NOT_FOUND_WITH_ID - The requested resource with the specified ID could not be found. + * @property {number} NotFoundCodes.RESOURCE_NOT_FOUND_WITH_PARAMETERS - The requested resource with the specified name could not be found. + * + * @property {Object} BadRequestCodes - Constants for bad request HTTP error codes. + * @property {number} BadRequestCodes.MISSING_REQUIRED_PARAMETER - Required parameter is missing from request. + * @property {number} BadRequestCodes.INVALID_PARAMETER_VALUE - Parameter value is invalid. + * @property {number} BadRequestCodes.UNSUPPORTED_PARAMETER - Request contains unsupported parameter. + * @property {number} BadRequestCodes.INVALID_CONTENT_TYPE - Content type of request is invalid. + * @property {number} BadRequestCodes.INVALID_REQUEST_BODY - Request body is invalid. + * @property {number} BadRequestCodes.RESOURCE_ALREADY_EXISTS - Resource already exists. + * @property {number} BadRequestCodes.RESOURCE_NOT_FOUND - Resource not found. + * @property {number} BadRequestCodes.REQUEST_TOO_LARGE - Request is too large. + * @property {number} BadRequestCodes.REQUEST_ENTITY_TOO_LARGE - Request entity is too large. + * @property {number} BadRequestCodes.REQUEST_URI_TOO_LONG - Request URI is too long. + * @property {number} BadRequestCodes.UNSUPPORTED_MEDIA_TYPE - Request contains unsupported media type. + * @property {number} BadRequestCodes.METHOD_NOT_ALLOWED - Request method is not allowed. + * @property {number} BadRequestCodes.HTTP_REQUEST_TIMEOUT - Request has timed out. + * @property {number} BadRequestCodes.VALIDATION_ERROR - Request validation error. + * @property {number} BadRequestCodes.UNEXPECTED_ERROR - Unexpected error occurred. + * @property {number} BadRequestCodes.INVALID_INPUT - Invalid input. + * + * @property {Object} UnauthorizedCodes - Constants for unauthorized HTTP error codes. + * @property {number} UnauthorizedCodes.UNAUTHORIZED_ACCESS - Unauthorized access to resource. + * @property {number} UnauthorizedCodes.INVALID_CREDENTIALS - Invalid credentials provided. + * @property {number} UnauthorizedCodes.JSON_WEB_TOKEN_ERROR - JSON web token error. + * @property {number} UnauthorizedCodes.AUTHENTICATION_FAILED - Authentication failed. + * @property {number} UnauthorizedCodes.ACCESS_TOKEN_EXPIRED - Access token has expired. + * @property {number} UnauthorizedCodes.TOKEN_EXPIRED_ERROR - Token has expired error. + * @property {number} UnauthorizedCodes.UNEXPECTED_ERROR - Unexpected error occurred. + * @property {number} UnauthorizedCodes.RESOURCE_NOT_FOUND - Resource not found. + * @property {number} UnauthorizedCodes.USER_NOT_VERIFIED - User not verified. + * @property {number} UnauthorizedCodes.REQUIRED_RE_AUTHENTICATION - Required re-authentication. + * @property {number} UnauthorizedCodes.INVALID_RESET_PASSWORD_TOKEN - Invalid reset password token. + * + * @property {Object} InternalServerErrorCodes - Constants for internal server error HTTP error codes. + * @property {number} InternalServerErrorCodes.INTERNAL_SERVER_ERROR - Internal server error. + * @property {number} InternalServerErrorCodes.DATABASE_ERROR - Database error. + * @property {number} InternalServerErrorCodes.NETWORK_ERROR - Network error. + * @property {number} InternalServerErrorCodes.THIRD_PARTY_SERVICE_ERROR - Third party service error. + * @property {number} InternalServerErrorCodes.SERVER_OVERLOAD - Server is overloaded. + * @property {number} InternalServerErrorCodes.UNEXPECTED_ERROR - Unexpected error occurred. + * + * @property {Object} ForbiddenCodes - Constants for forbidden HTTP error codes. + * @property {number} ForbiddenCodes.FORBIDDEN - Access to resource is forbidden. + * @property {number} ForbiddenCodes.MISSING_PERMISSIONS - User does not have the required permissions to access the resource. + * @property {number} ForbiddenCodes.EXCEEDED_RATE_LIMIT - User has exceeded the rate limit for accessing the resource. + * @property {number} ForbiddenCodes.RESOURCE_NOT_FOUND - The requested resource could not be found. + * @property {number} ForbiddenCodes.TEMPORARILY_UNAVAILABLE - The requested resource is temporarily unavailable. + */ +export class ExceptionConstants { + public static readonly NotFoundCodes = { + URL_NOT_FOUND: 10000, // The requested URL was not found on the server + RESOURCE_NOT_FOUND: 10001, // The requested resource could not be found + RESOURCE_NOT_FOUND_WITH_ID: 10002, // The requested resource with the specified ID could not be found + RESOURCE_NOT_FOUND_WITH_PARAMETERS: 10003, // The requested resource with the specified name could not be found + }; + + /** + * Constants for bad request HTTP error codes. + */ + public static readonly BadRequestCodes = { + MISSING_REQUIRED_PARAMETER: 20001, // Required parameter is missing from request + INVALID_PARAMETER_VALUE: 20002, // Parameter value is invalid + UNSUPPORTED_PARAMETER: 20003, // Request contains unsupported parameter + INVALID_CONTENT_TYPE: 20004, // Content type of request is invalid + INVALID_REQUEST_BODY: 20005, // Request body is invalid + RESOURCE_ALREADY_EXISTS: 20006, // Resource already exists + RESOURCE_NOT_FOUND: 20007, // Resource not found + REQUEST_TOO_LARGE: 20008, // Request is too large + REQUEST_ENTITY_TOO_LARGE: 20009, // Request entity is too large + REQUEST_URI_TOO_LONG: 20010, // Request URI is too long + UNSUPPORTED_MEDIA_TYPE: 20011, // Request contains unsupported media type + METHOD_NOT_ALLOWED: 20012, // Request method is not allowed + HTTP_REQUEST_TIMEOUT: 20013, // Request has timed out + VALIDATION_ERROR: 20014, // Request validation error + UNEXPECTED_ERROR: 20015, // Unexpected error occurred + INVALID_INPUT: 20016, // Invalid input + }; + + /** + * Constants for unauthorized HTTP error codes. + */ + public static readonly UnauthorizedCodes = { + UNAUTHORIZED_ACCESS: 30001, // Unauthorized access to resource + INVALID_CREDENTIALS: 30002, // Invalid credentials provided + JSON_WEB_TOKEN_ERROR: 30003, // JSON web token error + AUTHENTICATION_FAILED: 30004, // Authentication failed + ACCESS_TOKEN_EXPIRED: 30005, // Access token has expired + TOKEN_EXPIRED_ERROR: 30006, // Token has expired error + UNEXPECTED_ERROR: 30007, // Unexpected error occurred + RESOURCE_NOT_FOUND: 30008, // Resource not found + USER_NOT_VERIFIED: 30009, // User not verified + REQUIRED_RE_AUTHENTICATION: 30010, // Required re-authentication + INVALID_RESET_PASSWORD_TOKEN: 30011, // Invalid reset password token + }; + + /** + * Constants for internal server error HTTP error codes. + */ + public static readonly InternalServerErrorCodes = { + INTERNAL_SERVER_ERROR: 40001, // Internal server error + DATABASE_ERROR: 40002, // Database error + NETWORK_ERROR: 40003, // Network error + THIRD_PARTY_SERVICE_ERROR: 40004, // Third party service error + SERVER_OVERLOAD: 40005, // Server is overloaded + UNEXPECTED_ERROR: 40006, // Unexpected error occurred + }; + + /** + * Constants for forbidden HTTP error codes. + */ + public static readonly ForbiddenCodes = { + FORBIDDEN: 50001, // Access to resource is forbidden + MISSING_PERMISSIONS: 50002, // User does not have the required permissions to access the resource + EXCEEDED_RATE_LIMIT: 50003, // User has exceeded the rate limit for accessing the resource + RESOURCE_NOT_FOUND: 50004, // The requested resource could not be found + TEMPORARILY_UNAVAILABLE: 50005, // The requested resource is temporarily unavailable + }; +} diff --git a/src/core/exceptions/constants/index.ts b/src/core/exceptions/constants/index.ts new file mode 100644 index 0000000..ee7309e --- /dev/null +++ b/src/core/exceptions/constants/index.ts @@ -0,0 +1 @@ +export * from './exceptions.constants'; diff --git a/src/exceptions/forbidden.exception.ts b/src/core/exceptions/forbidden.exception.ts similarity index 61% rename from src/exceptions/forbidden.exception.ts rename to src/core/exceptions/forbidden.exception.ts index daad724..d8d3469 100644 --- a/src/exceptions/forbidden.exception.ts +++ b/src/core/exceptions/forbidden.exception.ts @@ -6,18 +6,57 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { HttpException, HttpStatus } from '@nestjs/common'; -// Import internal modules -import { ExceptionConstants } from './exceptions.constants'; -import { IException, IHttpForbiddenExceptionResponse } from './exceptions.interface'; +import { ExceptionConstants } from './constants'; +import { IException, IHttpForbiddenExceptionResponse } from './interfaces'; /** * A custom exception for forbidden errors. */ +/** + * Represents a ForbiddenException which extends the HttpException class. + * This exception is thrown when access to a resource is forbidden. + * + * @class + * @extends HttpException + * + * @property {number} code - The error code. + * @property {Error} cause - The error that caused this exception. + * @property {string} message - The error message. + * @property {string} description - The detailed description of the error. + * @property {string} timestamp - Timestamp of the exception. + * @property {string} traceId - Trace ID of the request. + * + * @constructor + * @param {IException} exception - An object containing the exception details. + * @param {string} exception.message - A string representing the error message. + * @param {Error} exception.cause - An object representing the cause of the error. + * @param {string} exception.description - A string describing the error in detail. + * @param {number} exception.code - A number representing internal status code which is helpful in future for frontend. + * + * @method setTraceId + * @param {string} traceId - A string representing the Trace ID. + * @description Sets the Trace ID of the ForbiddenException instance. + * + * @method generateHttpResponseBody + * @param {string} [message] - A string representing the message to include in the response body. + * @returns {IHttpForbiddenExceptionResponse} An object representing the HTTP response body. + * @description Generates an HTTP response body representing the ForbiddenException instance. + * + * @method static FORBIDDEN + * @param {string} [msg] - An optional error message. + * @returns {ForbiddenException} An instance of the ForbiddenException class. + * @description A static method to generate an exception forbidden error. + * + * @method static MISSING_PERMISSIONS + * @param {string} [msg] - An optional error message. + * @returns {ForbiddenException} An instance of the ForbiddenException class. + * @description A static method to generate an exception missing permissions error. + */ export class ForbiddenException extends HttpException { /** The error code. */ @ApiProperty({ enum: ExceptionConstants.ForbiddenCodes, - description: 'You do not have permission to perform this action.', + description: 'A unique code identifying the error.', example: ExceptionConstants.ForbiddenCodes.MISSING_PERMISSIONS, }) code: number; @@ -29,13 +68,14 @@ export class ForbiddenException extends HttpException { /** The error message. */ @ApiProperty({ description: 'Message for the exception', - example: 'You do not have permission to perform this action.', + example: 'Access to this resource is forbidden.', }) message: string; /** The detailed description of the error. */ @ApiProperty({ description: 'A description of the error message.', + example: 'You do not have permission to perform this action.', }) description: string; diff --git a/src/core/exceptions/gateway-timeout.exception.ts b/src/core/exceptions/gateway-timeout.exception.ts new file mode 100644 index 0000000..74806ac --- /dev/null +++ b/src/core/exceptions/gateway-timeout.exception.ts @@ -0,0 +1,107 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { HttpException, HttpStatus } from '@nestjs/common'; + +import { ExceptionConstants } from './constants'; +import { IException, IHttpGatewayTimeOutExceptionResponse } from './interfaces'; + +/** + * Represents a Gateway Timeout Exception. + * + * @class GatewayTimeoutException + * @extends {HttpException} + * + * @property {number} code - A unique code identifying the error. + * @property {Error} cause - Error object causing the exception. + * @property {string} message - Message for the exception. + * @property {string} [description] - A description of the error message. + * @property {string} timestamp - Timestamp of the exception. + * @property {string} traceId - Trace ID of the request. + * @property {string} path - Requested path. + * + * @constructor + * @param {IException} exception - The exception object containing error details. + * + * @method setTraceId + * @param {string} traceId - Sets the trace ID of the request. + * + * @method setPath + * @param {string} path - Sets the requested path. + * + * @method generateHttpResponseBody + * @param {string} [message] - Optional custom message for the response body. + * @returns {IHttpGatewayTimeOutExceptionResponse} - The HTTP response body for the exception. + */ +export class GatewayTimeoutException extends HttpException { + @ApiProperty({ + enum: ExceptionConstants.BadRequestCodes, + description: 'A unique code identifying the error.', + example: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, + }) + code: number; // Internal status code + + @ApiHideProperty() + cause: Error; // Error object causing the exception + + @ApiProperty({ + description: 'Message for the exception', + example: 'Gateway Timeout', + }) + message: string; // Message for the exception + + @ApiProperty({ + description: 'A description of the error message.', + example: 'The server is taking too long to respond.', + }) + description?: string; // Description of the exception + + @ApiProperty({ + description: 'Timestamp of the exception', + format: 'date-time', + example: '2022-12-31T23:59:59.999Z', + }) + timestamp: string; // Timestamp of the exception + + @ApiProperty({ + description: 'Trace ID of the request', + example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', + }) + traceId: string; // Trace ID of the request + + @ApiProperty({ + description: 'Request path', + example: '/', + }) + path: string; // requested path + + constructor(exception: IException) { + super(exception.message, HttpStatus.BAD_REQUEST, { + cause: exception.cause, + description: exception.description, + }); + + this.message = exception.message; + this.cause = exception.cause ?? new Error(); + this.description = exception.description; + this.code = exception.code ?? HttpStatus.BAD_REQUEST; + this.timestamp = new Date().toISOString(); + } + + setTraceId = (traceId: string) => { + this.traceId = traceId; + }; + + setPath = (path: string) => { + this.path = path; + }; + + generateHttpResponseBody = (message?: string): IHttpGatewayTimeOutExceptionResponse => { + return { + message: message || this.message, + description: this.description, + timestamp: this.timestamp, + code: this.code, + traceId: this.traceId, + path: this.path, + }; + }; +} diff --git a/src/core/exceptions/index.ts b/src/core/exceptions/index.ts new file mode 100644 index 0000000..e1236c3 --- /dev/null +++ b/src/core/exceptions/index.ts @@ -0,0 +1,9 @@ +export * from './constants'; +export * from './interfaces'; +export * from './all.exception'; +export * from './bad-request.exception'; +export * from './forbidden.exception'; +export * from './gateway-timeout.exception'; +export * from './internal-server-error.exception'; +export * from './not-found.exception'; +export * from './unauthorized.exception'; diff --git a/src/core/exceptions/interfaces/exceptions.interface.ts b/src/core/exceptions/interfaces/exceptions.interface.ts new file mode 100644 index 0000000..5face96 --- /dev/null +++ b/src/core/exceptions/interfaces/exceptions.interface.ts @@ -0,0 +1,155 @@ +/** + * Interface representing the structure of an exception. + * + * @interface IException + * + * @property {string} message - A brief message describing the error. + * @property {number} [code] - The error code associated with the exception. + * @property {Error} [cause] - The cause of the exception. + * @property {string} [description] - A detailed description of the error. + */ +export interface IException { + message: string; + code?: number; + cause?: Error; + description?: string; +} + +/** + * Interface representing the structure of a not found exception response. + * + * @interface IHttpNotFoundExceptionResponse + * + * @property {number} code - The HTTP status code of the not found error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} path - The path of the request that caused the not found error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpNotFoundExceptionResponse { + code: number; + message: string; + description: string; + path?: string; + timestamp: string; + traceId: string; +} + +/** + * Interface representing the structure of a bad request exception response. + * + * @interface IHttpBadRequestExceptionResponse + * + * @property {number} code - The HTTP status code of the bad request. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpBadRequestExceptionResponse { + code: number; + message: string; + description: string; + timestamp: string; + traceId: string; +} + +/** + * Interface representing the structure of a not found exception response. + * + * @interface IHttpInternalServerErrorExceptionResponse + * + * @property {number} code - The HTTP status code of the not found error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpInternalServerErrorExceptionResponse { + code: number; + message: string; + description: string; + timestamp: string; + traceId: string; +} + +/** + * Interface representing the structure of an unauthorized exception response. + * + * @interface IHttpUnauthorizedExceptionResponse + * + * @property {number} code - The HTTP status code of the unauthorized error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpUnauthorizedExceptionResponse { + code: number; + message: string; + description: string; + timestamp: string; + traceId: string; +} + +/** + * Interface representing the structure of a forbidden exception response. + * + * @interface IHttpForbiddenExceptionResponse + * + * @property {number} code - The HTTP status code of the forbidden error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpForbiddenExceptionResponse { + code: number; + message: string; + description: string; + timestamp: string; + traceId: string; +} + +/** + * Interface representing the structure of a conflict exception response. + * + * @interface IHttpGatewayTimeOutExceptionResponse + * + * @property {number} code - The HTTP status code of the conflict error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + * @property {string} path - The path of the request that caused the conflict. + */ +export interface IHttpGatewayTimeOutExceptionResponse { + code: number; + message: string; + description?: string; + timestamp: string; + traceId: string; + path: string; +} + +/** + * Interface representing the structure of a request timeout exception response. + * + * @interface IHttpAllExceptionResponse + * + * @property {number} code - The HTTP status code of the request timeout error. + * @property {string} message - A brief message describing the error. + * @property {string} description - A detailed description of the error. + * @property {unknown} error - The error that caused the exception. + * @property {string} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ +export interface IHttpAllExceptionResponse { + code: number; + message: string; + description: string; + error?: unknown; + timestamp: string; + traceId: string; +} diff --git a/src/core/exceptions/interfaces/index.ts b/src/core/exceptions/interfaces/index.ts new file mode 100644 index 0000000..7b8bb20 --- /dev/null +++ b/src/core/exceptions/interfaces/index.ts @@ -0,0 +1 @@ +export * from './exceptions.interface'; diff --git a/src/exceptions/internal-server-error.exception.ts b/src/core/exceptions/internal-server-error.exception.ts similarity index 83% rename from src/exceptions/internal-server-error.exception.ts rename to src/core/exceptions/internal-server-error.exception.ts index 5b7eaa7..8bbbbee 100644 --- a/src/exceptions/internal-server-error.exception.ts +++ b/src/core/exceptions/internal-server-error.exception.ts @@ -1,12 +1,18 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { HttpException, HttpStatus } from '@nestjs/common'; -// Import internal files & modules -import { ExceptionConstants } from './exceptions.constants'; -import { IException, IHttpInternalServerErrorExceptionResponse } from './exceptions.interface'; +import { ExceptionConstants } from './constants'; +import { IException, IHttpInternalServerErrorExceptionResponse } from './interfaces'; // Exception class for Internal Server Error +/** + * Represents an internal server error exception. + * Extends the HttpException class. + */ export class InternalServerErrorException extends HttpException { + /** + * A unique code identifying the error. + */ @ApiProperty({ enum: ExceptionConstants.InternalServerErrorCodes, description: 'A unique code identifying the error.', @@ -14,15 +20,24 @@ export class InternalServerErrorException extends HttpException { }) code: number; // Internal status code + /** + * Error object causing the exception. + */ @ApiHideProperty() cause: Error; // Error object causing the exception + /** + * Message for the exception. + */ @ApiProperty({ description: 'Message for the exception', example: 'An unexpected error occurred while processing your request.', }) message: string; // Message for the exception + /** + * A description of the error message. + */ @ApiProperty({ description: 'A description of the error message.', example: @@ -30,6 +45,9 @@ export class InternalServerErrorException extends HttpException { }) description: string; // Description of the exception + /** + * Timestamp of the exception. + */ @ApiProperty({ description: 'Timestamp of the exception', format: 'date-time', @@ -37,6 +55,9 @@ export class InternalServerErrorException extends HttpException { }) timestamp: string; // Timestamp of the exception + /** + * Trace ID of the request. + */ @ApiProperty({ description: 'Trace ID of the request', example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', @@ -87,9 +108,9 @@ export class InternalServerErrorException extends HttpException { }; /** - * Returns a new instance of InternalServerErrorException with a standard error message and code - * @param error Error object causing the exception - * @returns A new instance of InternalServerErrorException + * Returns a new instance of InternalServerErrorException with a standard error message and code. + * @param error Error object causing the exception. + * @returns A new instance of InternalServerErrorException. */ static INTERNAL_SERVER_ERROR = (error: any) => { return new InternalServerErrorException({ @@ -100,9 +121,9 @@ export class InternalServerErrorException extends HttpException { }; /** - * Returns a new instance of InternalServerErrorException with a custom error message and code - * @param error Error object causing the exception - * @returns A new instance of InternalServerErrorException + * Returns a new instance of InternalServerErrorException with a custom error message and code. + * @param error Error object causing the exception. + * @returns A new instance of InternalServerErrorException. */ static UNEXPECTED_ERROR = (error: any) => { return new InternalServerErrorException({ diff --git a/src/core/exceptions/not-found.exception.ts b/src/core/exceptions/not-found.exception.ts new file mode 100644 index 0000000..2a6cac9 --- /dev/null +++ b/src/core/exceptions/not-found.exception.ts @@ -0,0 +1,156 @@ +// Import required modules +import { ApiHideProperty, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { HttpException, HttpStatus } from '@nestjs/common'; + +import { ExceptionConstants } from './constants'; +import { IException, IHttpNotFoundExceptionResponse } from './interfaces'; + +export class NotFoundException extends HttpException { + /** The error code. */ + @ApiProperty({ + enum: ExceptionConstants.NotFoundCodes, + description: 'A unique code identifying the error.', + example: ExceptionConstants.NotFoundCodes.RESOURCE_NOT_FOUND, + }) + code: number; + + /** The error that caused this exception. */ + @ApiHideProperty() + cause: Error; + + /** The error message. */ + @ApiProperty({ + description: 'Message for the exception', + example: 'Resource not found', + }) + message: string; + + /** The detailed description of the error. */ + @ApiProperty({ + description: 'A description of the error message.', + example: 'The requested resource was not found.', + }) + description: string; + + /** The path of the request that caused the not found error. */ + @ApiPropertyOptional({ + description: 'The path of the request that caused the not found error.', + example: '/api/v1/users', + }) + path?: string; + + /** The timestamp of the exception. */ + @ApiProperty({ + description: 'Timestamp of the exception', + format: 'date-time', + example: '2022-12-31T23:59:59.999Z', + }) + timestamp: string; + + /** Trace ID of the request */ + @ApiProperty({ + description: 'Trace ID of the request', + example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', + }) + traceId: string; // Trace ID of the request + + /** + * Constructs a new NotFoundException object. + * @param exception An object containing the exception details. + * - message: A string representing the error message. + * - cause: An object representing the cause of the error. + * - description: A string describing the error in detail. + * - code: A number representing internal status code which helpful in future for frontend + */ + constructor(exception: IException) { + super(exception.message, HttpStatus.NOT_FOUND, { + cause: exception.cause, + description: exception.description, + }); + + this.message = exception.message; + this.cause = exception.cause; + this.description = exception.description; + this.code = exception.code || ExceptionConstants.NotFoundCodes.RESOURCE_NOT_FOUND; + this.timestamp = new Date().toISOString(); + } + + /** + * Set the path of the NotFoundException instance. + * @param path A string representing the path of the request. + */ + setPath(path: string): void { + this.path = path; + } + + /** + * Set the Trace ID of the NotFoundException instance. + * @param traceId A string representing the Trace ID. + */ + setTraceId(traceId: string): void { + this.traceId = traceId; + } + + generateHttpResponseBody(message?: string): IHttpNotFoundExceptionResponse { + if (this.code === ExceptionConstants.NotFoundCodes.URL_NOT_FOUND && this.path !== undefined) { + this.message = `The URL not found`; + if (message) { + this.message = message; + } + this.description = `The requested URL was not found: ${this.path}`; + + return { + code: this.code, + message: this.message, + description: this.description, + path: this.path, + timestamp: this.timestamp, + traceId: this.traceId, + }; + } else { + return { + code: this.code, + message: message || this.message, + description: this.description, + timestamp: this.timestamp, + traceId: this.traceId, + }; + } + } + + static fromError(error: Error): NotFoundException { + return new NotFoundException({ + message: error.message, + cause: error, + description: error.stack, + }); + } + + static RESOURCE_NOT_FOUND = (message?: string): NotFoundException => { + return new NotFoundException({ + message: message || 'Resource not found', + code: ExceptionConstants.NotFoundCodes.RESOURCE_NOT_FOUND, + }); + }; + + static RESOURCE_NOT_FOUND_WITH_ID = (message?: string): NotFoundException => { + return new NotFoundException({ + message: message || 'Resource not found with the specified ID', + code: ExceptionConstants.NotFoundCodes.RESOURCE_NOT_FOUND_WITH_ID, + }); + }; + + static RESOURCE_NOT_FOUND_WITH_PARAMETERS = (message?: string): NotFoundException => { + return new NotFoundException({ + message: message || 'Resource not found with the specified parameters', + code: ExceptionConstants.NotFoundCodes.RESOURCE_NOT_FOUND_WITH_PARAMETERS, + }); + }; + + static URL_NOT_FOUND = (message?: string): NotFoundException => { + return new NotFoundException({ + message: message || 'Resource not found with the specified name', + code: ExceptionConstants.NotFoundCodes.URL_NOT_FOUND, + }); + }; +} diff --git a/src/exceptions/unauthorized.exception.ts b/src/core/exceptions/unauthorized.exception.ts similarity index 70% rename from src/exceptions/unauthorized.exception.ts rename to src/core/exceptions/unauthorized.exception.ts index abe2f67..7f606cc 100644 --- a/src/exceptions/unauthorized.exception.ts +++ b/src/core/exceptions/unauthorized.exception.ts @@ -6,13 +6,72 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { HttpException, HttpStatus } from '@nestjs/common'; -// Import internal modules -import { ExceptionConstants } from './exceptions.constants'; -import { IException, IHttpUnauthorizedExceptionResponse } from './exceptions.interface'; +import { ExceptionConstants } from './constants'; +import { IException, IHttpUnauthorizedExceptionResponse } from './interfaces'; /** * A custom exception for unauthorized access errors. */ +/** + * Represents an UnauthorizedException which extends the HttpException class. + * This exception is thrown when an unauthorized access attempt is made. + * + * @class UnauthorizedException + * @extends {HttpException} + * + * @property {number} code - The error code. + * @property {Error} cause - The error that caused this exception. + * @property {string} message - The error message. + * @property {string} description - The detailed description of the error. + * @property {string} timestamp - Timestamp of the exception. + * @property {string} traceId - Trace ID of the request. + * + * @constructor + * @param {IException} exception - An object containing the exception details. + * @param {string} exception.message - A string representing the error message. + * @param {Error} exception.cause - An object representing the cause of the error. + * @param {string} exception.description - A string describing the error in detail. + * @param {number} exception.code - A number representing internal status code which is helpful in the future for frontend. + * + * @method setTraceId + * @param {string} traceId - A string representing the Trace ID. + * + * @method generateHttpResponseBody + * @param {string} [message] - A string representing the message to include in the response body. + * @returns {IHttpUnauthorizedExceptionResponse} - An object representing the HTTP response body. + * + * @method static TOKEN_EXPIRED_ERROR + * @param {string} [msg] - An optional error message. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + * + * @method static JSON_WEB_TOKEN_ERROR + * @param {string} [msg] - An optional error message. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + * + * @method static UNAUTHORIZED_ACCESS + * @param {string} [description] - An optional detailed description of the error. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + * + * @method static RESOURCE_NOT_FOUND + * @param {string} [msg] - Optional message for the exception. + * @returns {UnauthorizedException} - A UnauthorizedException with the appropriate error code and message. + * + * @method static USER_NOT_VERIFIED + * @param {string} [msg] - Optional message for the exception. + * @returns {UnauthorizedException} - A UnauthorizedException with the appropriate error code and message. + * + * @method static UNEXPECTED_ERROR + * @param {any} error - The error that caused this exception. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + * + * @method static REQUIRED_RE_AUTHENTICATION + * @param {string} [msg] - An optional error message. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + * + * @method static INVALID_RESET_PASSWORD_TOKEN + * @param {string} [msg] - An optional error message. + * @returns {UnauthorizedException} - An instance of the UnauthorizedException class. + */ export class UnauthorizedException extends HttpException { /** The error code. */ @ApiProperty({ diff --git a/src/core/filters/all-exception.filter.ts b/src/core/filters/all-exception.filter.ts new file mode 100644 index 0000000..826d348 --- /dev/null +++ b/src/core/filters/all-exception.filter.ts @@ -0,0 +1,62 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, UnprocessableEntityException } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; + +import { AllException, ExceptionConstants } from '../exceptions'; + +/** + * A filter to catch all exceptions and format the response. + */ +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + + constructor(private readonly httpAdapterHost: HttpAdapterHost) {} + + /** + * Method to handle caught exceptions. + * @param exception - The exception that was thrown. + * @param host - The arguments host. + */ + catch(exception: unknown, host: ArgumentsHost): void { + this.logger.error(exception); + + const { httpAdapter } = this.httpAdapterHost; + const ctx = host.switchToHttp(); + + if (exception instanceof UnprocessableEntityException) { + const unprocessableException = exception as UnprocessableEntityException; + const httpStatus = unprocessableException.getStatus(); + const response = unprocessableException.getResponse(); + + const exc = new AllException({ + code: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, + message: unprocessableException.name, + description: unprocessableException.message, + cause: unprocessableException, + }); + exc.setTraceId(ctx.getRequest().id); + exc.setError(response && response['validationErrors'] ? response['validationErrors'] : response); + + const responseBody = exc.generateHttpResponseBody(); + + httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); + } else { + // Determine the HTTP status code + const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + + const request = ctx.getRequest(); + + // Construct the response body + const responseBody = { + error: (exception as any).code || 'INTERNAL_SERVER_ERROR', + message: (exception as any).message || 'An unexpected error occurred', + description: (exception as any).description || null, + timestamp: new Date().toISOString(), + traceId: request.id || null, + }; + + // Send the response + httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); + } + } +} diff --git a/src/filters/bad-request-exception.filter.ts b/src/core/filters/bad-request-exception.filter.ts similarity index 83% rename from src/filters/bad-request-exception.filter.ts rename to src/core/filters/bad-request-exception.filter.ts index 47a777f..b29f44d 100644 --- a/src/filters/bad-request-exception.filter.ts +++ b/src/core/filters/bad-request-exception.filter.ts @@ -1,14 +1,14 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; -import { BadRequestException } from '../exceptions/bad-request.exception'; +import { BadRequestException, IHttpBadRequestExceptionResponse } from '../exceptions'; /** * A filter to handle `BadRequestException`. */ @Catch(BadRequestException) export class BadRequestExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(BadRequestException.name); + private readonly logger: Logger = new Logger(BadRequestExceptionFilter.name); /** * Constructs a new instance of `BadRequestExceptionFilter`. @@ -23,7 +23,7 @@ export class BadRequestExceptionFilter implements ExceptionFilter { */ catch(exception: BadRequestException, host: ArgumentsHost): void { // Logs the exception details at the verbose level. - this.logger.verbose(exception); + this.logger.verbose(`BadRequestException: ${exception.message}`); // In certain situations `httpAdapter` might not be available in the constructor method, // thus we should resolve it here. @@ -42,7 +42,7 @@ export class BadRequestExceptionFilter implements ExceptionFilter { exception.setTraceId(request.id); // Constructs the response body object. - const responseBody = exception.generateHttpResponseBody(); + const responseBody: IHttpBadRequestExceptionResponse = exception.generateHttpResponseBody(); // Uses the HTTP adapter to send the response with the constructed response body // and the HTTP status code. diff --git a/src/filters/forbidden-exception.filter.ts b/src/core/filters/forbidden-exception.filter.ts similarity index 70% rename from src/filters/forbidden-exception.filter.ts rename to src/core/filters/forbidden-exception.filter.ts index f7afa0f..187f267 100644 --- a/src/filters/forbidden-exception.filter.ts +++ b/src/core/filters/forbidden-exception.filter.ts @@ -1,15 +1,25 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; -import { ForbiddenException } from '../exceptions'; +import { ForbiddenException, IHttpForbiddenExceptionResponse } from '../exceptions'; /** - * Exception filter to handle unauthorized exceptions + * A filter to handle ForbiddenException in a NestJS application. + * + * @class + * @implements {ExceptionFilter} + * @decorator {Catch(ForbiddenException)} */ @Catch(ForbiddenException) export class ForbiddenExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(ForbiddenExceptionFilter.name); + private readonly logger: Logger = new Logger(ForbiddenExceptionFilter.name); + /** + * Creates an instance of ForbiddenExceptionFilter. + * + * @constructor + * @param {HttpAdapterHost} httpAdapterHost + */ constructor(private readonly httpAdapterHost: HttpAdapterHost) {} /** @@ -35,7 +45,7 @@ export class ForbiddenExceptionFilter implements ExceptionFilter { exception.setTraceId(request.id); // Constructs the response body object. - const responseBody = exception.generateHttpResponseBody(); + const responseBody: IHttpForbiddenExceptionResponse = exception.generateHttpResponseBody(); // Uses the HTTP adapter to send the response with the constructed response body // and the HTTP status code. diff --git a/src/core/filters/gateway-timeout.exception.filter.ts b/src/core/filters/gateway-timeout.exception.filter.ts new file mode 100644 index 0000000..ce4061c --- /dev/null +++ b/src/core/filters/gateway-timeout.exception.filter.ts @@ -0,0 +1,44 @@ +import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; + +import { GatewayTimeoutException, IHttpGatewayTimeOutExceptionResponse } from 'src/core/exceptions'; + +/** + * @class GatewayTimeOutExceptionFilter + * @implements {ExceptionFilter} + * @description Exception filter to handle GatewayTimeoutException and format the response. + */ +@Catch(GatewayTimeoutException) +export class GatewayTimeOutExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GatewayTimeoutException.name); + + /** + * @constructor + * @param {HttpAdapterHost} httpAdapterHost - The HTTP adapter host. + */ + constructor(private readonly httpAdapterHost: HttpAdapterHost) {} + + /** + * @method catch + * @description Handles the GatewayTimeoutException and sends a formatted response. + * @param {GatewayTimeoutException} exception - The caught exception. + * @param {ArgumentsHost} host - The arguments host. + * @returns {void} + */ + catch(exception: GatewayTimeoutException, host: ArgumentsHost): void { + this.logger.debug('exception'); + + const { httpAdapter } = this.httpAdapterHost; + + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const httpStatus = exception.getStatus(); + const traceId = request.headers.get('x-request-id') || ''; + exception.setTraceId(traceId); + exception.setPath(httpAdapter.getRequestUrl(ctx.getRequest())); + + const responseBody: IHttpGatewayTimeOutExceptionResponse = exception.generateHttpResponseBody(); + + httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); + } +} diff --git a/src/filters/index.ts b/src/core/filters/index.ts similarity index 62% rename from src/filters/index.ts rename to src/core/filters/index.ts index dacd3b2..c908c00 100644 --- a/src/filters/index.ts +++ b/src/core/filters/index.ts @@ -1,6 +1,9 @@ -export * from './not-found-exception.filter'; export * from './all-exception.filter'; export * from './bad-request-exception.filter'; -export * from './unauthorized-exception.filter'; export * from './forbidden-exception.filter'; +export * from './gateway-timeout.exception.filter'; +export * from './internal-server-error-exception.filter'; +export * from './not-found-exception.filter'; +export * from './unauthorized-exception.filter'; +export * from './unprocessable-entity-exception.filter'; export * from './validator-exception.filter'; diff --git a/src/filters/internal-server-error-exception.filter.ts b/src/core/filters/internal-server-error-exception.filter.ts similarity index 87% rename from src/filters/internal-server-error-exception.filter.ts rename to src/core/filters/internal-server-error-exception.filter.ts index c4cede5..89e7afc 100644 --- a/src/filters/internal-server-error-exception.filter.ts +++ b/src/core/filters/internal-server-error-exception.filter.ts @@ -2,13 +2,18 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; -import { InternalServerErrorException } from '../exceptions/internal-server-error.exception'; +import { IHttpInternalServerErrorExceptionResponse, InternalServerErrorException } from '../exceptions'; /** * A filter to handle `InternalServerErrorException`. */ @Catch(InternalServerErrorException) export class InternalServerErrorExceptionFilter implements ExceptionFilter { + /** + * @private + * @readonly + * @type {${1:*}} + */ private readonly logger = new Logger(InternalServerErrorException.name); /** @@ -43,7 +48,7 @@ export class InternalServerErrorExceptionFilter implements ExceptionFilter { exception.setTraceId(request.id); // Constructs the response body object. - const responseBody = exception.generateHttpResponseBody(); + const responseBody: IHttpInternalServerErrorExceptionResponse = exception.generateHttpResponseBody(); // Uses the HTTP adapter to send the response with the constructed response body // and the HTTP status code. diff --git a/src/filters/not-found-exception.filter.ts b/src/core/filters/not-found-exception.filter.ts similarity index 53% rename from src/filters/not-found-exception.filter.ts rename to src/core/filters/not-found-exception.filter.ts index 1abe4db..f28055f 100644 --- a/src/filters/not-found-exception.filter.ts +++ b/src/core/filters/not-found-exception.filter.ts @@ -1,12 +1,14 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; +import { ExceptionConstants, IHttpNotFoundExceptionResponse, NotFoundException } from '../exceptions'; + /** * Catches all exceptions thrown by the application and sends an appropriate HTTP response. */ @Catch(NotFoundException) export class NotFoundExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(NotFoundExceptionFilter.name); + private readonly logger: Logger = new Logger(NotFoundExceptionFilter.name); /** * Creates an instance of `NotFoundExceptionFilter`. @@ -22,26 +24,32 @@ export class NotFoundExceptionFilter implements ExceptionFilter { * @param {ArgumentsHost} host - the arguments host * @returns {void} */ - catch(exception: any, host: ArgumentsHost): void { + catch(exception: NotFoundException, host: ArgumentsHost): void { // Log the exception. + this.logger.warn(exception); // In certain situations `httpAdapter` might not be available in the // constructor method, thus we should resolve it here. const { httpAdapter } = this.httpAdapterHost; const ctx = host.switchToHttp(); - - const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const httpStatus = exception.getStatus(); const request = ctx.getRequest(); + if (exception.code === ExceptionConstants.NotFoundCodes.URL_NOT_FOUND) { + const path = httpAdapter.getRequestUrl(request); + // Sets the path from the request object to the exception. + exception.setPath(path); + // Log the exception. + this.logger.warn(`The requested URL was not found: ${path}`); + } + + // Sets the trace ID from the request object to the exception. + exception.setTraceId(request.id); + // Construct the response body. - const responseBody = { - error: exception.code, - message: exception.message, - description: exception.description, - traceId: request.id, - }; + const responseBody: IHttpNotFoundExceptionResponse = exception.generateHttpResponseBody(); // Send the HTTP response. httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); diff --git a/src/filters/unauthorized-exception.filter.ts b/src/core/filters/unauthorized-exception.filter.ts similarity index 75% rename from src/filters/unauthorized-exception.filter.ts rename to src/core/filters/unauthorized-exception.filter.ts index ac5d2ae..7c3780b 100644 --- a/src/filters/unauthorized-exception.filter.ts +++ b/src/core/filters/unauthorized-exception.filter.ts @@ -1,15 +1,25 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; -import { UnauthorizedException } from '../exceptions/unauthorized.exception'; +import { IHttpUnauthorizedExceptionResponse, UnauthorizedException } from '../exceptions'; /** - * Exception filter to handle unauthorized exceptions + * A filter to handle unauthorized exceptions. + * + * @class + * @implements {ExceptionFilter} + * @decorator {Catch(UnauthorizedException)} */ @Catch(UnauthorizedException) export class UnauthorizedExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(UnauthorizedExceptionFilter.name); + /** + * Creates an instance of UnauthorizedExceptionFilter. + * + * @constructor + * @param {HttpAdapterHost} httpAdapterHost + */ constructor(private readonly httpAdapterHost: HttpAdapterHost) {} /** @@ -35,7 +45,7 @@ export class UnauthorizedExceptionFilter implements ExceptionFilter { exception.setTraceId(request.id); // Constructs the response body object. - const responseBody = exception.generateHttpResponseBody(); + const responseBody: IHttpUnauthorizedExceptionResponse = exception.generateHttpResponseBody(); // Uses the HTTP adapter to send the response with the constructed response body // and the HTTP status code. diff --git a/src/core/filters/unprocessable-entity-exception.filter.ts b/src/core/filters/unprocessable-entity-exception.filter.ts new file mode 100644 index 0000000..9928ef5 --- /dev/null +++ b/src/core/filters/unprocessable-entity-exception.filter.ts @@ -0,0 +1,37 @@ +import { ExceptionFilter, Catch, ArgumentsHost, UnprocessableEntityException, Logger } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; + +import { Response } from 'express'; + +import { AllException, ExceptionConstants } from '../exceptions'; + +@Catch(UnprocessableEntityException) +export class UnprocessableEntityExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(UnprocessableEntityExceptionFilter.name); + + constructor(private readonly httpAdapterHost: HttpAdapterHost) {} + + catch(exception: UnprocessableEntityException, host: ArgumentsHost): void { + this.logger.error(exception); + + const { httpAdapter } = this.httpAdapterHost; + const ctx = host.switchToHttp(); + + const httpStatus = exception.getStatus(); + + const response = ctx.getResponse(); + + const finalException: AllException = new AllException({ + code: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, + message: exception.name, + description: exception.message, + cause: exception, + }); + finalException.setTraceId(ctx.getRequest().id); + finalException.setError(response && response['validationErrors'] ? response['validationErrors'] : response); + + const responseBody = finalException.generateHttpResponseBody(); + + httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); + } +} diff --git a/src/filters/validator-exception.filter.ts b/src/core/filters/validator-exception.filter.ts similarity index 85% rename from src/filters/validator-exception.filter.ts rename to src/core/filters/validator-exception.filter.ts index 3d9d620..19151af 100644 --- a/src/filters/validator-exception.filter.ts +++ b/src/core/filters/validator-exception.filter.ts @@ -1,16 +1,27 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; + import { ValidationError } from 'class-validator'; import { BadRequestException } from '../exceptions/bad-request.exception'; /** - * An exception filter to handle validation errors thrown by class-validator. + * A filter to handle validation exceptions thrown by the application. + * + * @class + * @implements {ExceptionFilter} + * @decorator {Catch(ValidationError)} */ @Catch(ValidationError) export class ValidationExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(ValidationExceptionFilter.name); + /** + * Creates an instance of ValidationExceptionFilter. + * + * @constructor + * @param {HttpAdapterHost} httpAdapterHost + */ constructor(private readonly httpAdapterHost: HttpAdapterHost) {} /** diff --git a/src/core/interceptors/index.ts b/src/core/interceptors/index.ts new file mode 100644 index 0000000..9e62c4a --- /dev/null +++ b/src/core/interceptors/index.ts @@ -0,0 +1,3 @@ +export * from './response.interceptor'; +export * from './serializer.interceptor'; +export * from './timeout.interceptor'; diff --git a/src/core/interceptors/response.interceptor.ts b/src/core/interceptors/response.interceptor.ts new file mode 100644 index 0000000..1b41ac4 --- /dev/null +++ b/src/core/interceptors/response.interceptor.ts @@ -0,0 +1,24 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; + +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { ResponseDto } from 'src/shared'; + +@Injectable() +export class HttpResponseInterceptor implements NestInterceptor { + /** + * Intercept the request and add the timestamp + * @param context {ExecutionContext} + * @param next {CallHandler} + * @returns { payload:Response, timestamp: string } + */ + intercept(context: ExecutionContext, next: CallHandler): Observable> { + const timestamp = new Date().toISOString(); + return next.handle().pipe( + map((payload) => { + return { payload, timestamp }; + }), + ); + } +} diff --git a/src/core/interceptors/serializer.interceptor.ts b/src/core/interceptors/serializer.interceptor.ts new file mode 100644 index 0000000..13ff71c --- /dev/null +++ b/src/core/interceptors/serializer.interceptor.ts @@ -0,0 +1,20 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; + +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { deepResolvePromisesUtil } from 'src/utils'; + +@Injectable() +export class SerializerInterceptor implements NestInterceptor { + /** + * Intercepts the request and serializes the response data by resolving any promises. + * + * @param {ExecutionContext} context - The execution context of the request. + * @param {CallHandler} next - The next handler in the request pipeline. + * @returns {Observable} An observable of the serialized response data. + */ + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe(map((data) => deepResolvePromisesUtil(data))); + } +} diff --git a/src/core/interceptors/timeout.interceptor.ts b/src/core/interceptors/timeout.interceptor.ts new file mode 100644 index 0000000..2da169f --- /dev/null +++ b/src/core/interceptors/timeout.interceptor.ts @@ -0,0 +1,44 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; + +import { Observable, TimeoutError, throwError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; + +import { ExceptionConstants, GatewayTimeoutException } from 'src/core/exceptions'; + +/** + * TimeoutInterceptor is a NestJS interceptor that applies a timeout to the request handling process. + * If the request takes longer than the specified time, it throws a GatewayTimeoutException. + * + * @class + * @implements {NestInterceptor} + * + * @constructor + * @param {number} millisec - The timeout duration in milliseconds. + * + * @method + * @name intercept + * @param {ExecutionContext} context - The execution context of the request. + * @param {CallHandler} next - The next handler in the request pipeline. + * @returns {Observable} - An observable that either completes within the specified timeout or throws a GatewayTimeoutException. + */ +@Injectable() +export class TimeoutInterceptor implements NestInterceptor { + constructor(private readonly millisec: number) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + timeout(this.millisec), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new GatewayTimeoutException({ + message: 'Gateway Timeout', + cause: new Error('Gateway Timeout'), + code: ExceptionConstants.BadRequestCodes.INVALID_INPUT, + description: 'Gateway Timeout', + }); + } + return throwError(() => err); + }), + ); + } +} diff --git a/src/core/log/log.module.ts b/src/core/log/log.module.ts new file mode 100644 index 0000000..f076dbd --- /dev/null +++ b/src/core/log/log.module.ts @@ -0,0 +1,143 @@ +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { IncomingMessage, ServerResponse } from 'http'; + +import { LoggerModule } from 'nestjs-pino'; + +import { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from 'src/config/app.config'; + +import { RequestLoggerMiddleware } from '../middlewares'; + +/** + * LogModule is a NestJS module that configures the logging mechanism for the application. + * It uses the LoggerModule with asynchronous configuration to set up logging based on the application's environment and configuration settings. + * + * @module LogModule + * + * @description + * This module imports the LoggerModule and configures it using the `forRootAsync` method. + * It injects the `ConfigService` to retrieve configuration values for the logging setup. + * + * @property {Array} imports - An array of modules to import. + * @property {Array} controllers - An array of controllers to include in the module. + * @property {Array} providers - An array of providers to include in the module. + * @property {Array} exports - An array of providers to export from the module. + * + * @method configure + * @description + * Configures middleware for the module. Applies the `RequestLoggerMiddleware` to all routes. + * + * @param {MiddlewareConsumer} consumer - The middleware consumer to configure. + * + * @example + * // Example configuration for LoggerModule + * LoggerModule.forRootAsync({ + * inject: [ConfigService], + * useFactory: async (configService: ConfigService) => { + * const appEnvironment = configService.get('app.env', { infer: true }); + * const logLevel = configService.get('app.logLevel', { infer: true }); + * const clusteringEnabled = configService.get('infra.clusteringEnabled', { infer: true }); + * return { + * exclude: [], + * pinoHttp: { + * genReqId: () => crypto.randomUUID(), + * autoLogging: true, + * base: clusteringEnabled === 'true' ? { pid: process.pid } : {}, + * customAttributeKeys: { + * responseTime: 'timeSpent', + * }, + * level: logLevel || (appEnvironment === E_APP_ENVIRONMENTS.PRODUCTION ? E_APP_LOG_LEVELS.INFO : E_APP_LOG_LEVELS.TRACE), + * serializers: { + * req(request: IncomingMessage) { + * return { + * method: request.method, + * url: request.url, + * id: request.id, + * }; + * }, + * res(reply: ServerResponse) { + * return { + * statusCode: reply.statusCode, + * }; + * }, + * }, + * transport: appEnvironment !== E_APP_ENVIRONMENTS.PRODUCTION + * ? { + * target: 'pino-pretty', + * options: { + * translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', + * }, + * } + * : null, + * }, + * }; + * }, + * }); + */ +@Module({ + imports: [ + // Configure logging + LoggerModule.forRootAsync({ + inject: [ConfigService], // Inject the ConfigService into the factory function + useFactory: async (configService: ConfigService) => { + const appEnvironment = configService.get('app.env', { + infer: true, + }); + const logLevel = configService.get('app.logLevel', { + infer: true, + }); + const clusteringEnabled = configService.get('infra.clusteringEnabled', { + infer: true, + }); + return { + exclude: [], // Exclude specific path from the logs and may not work for e2e testing + pinoHttp: { + genReqId: () => crypto.randomUUID(), // Generate a random UUID for each incoming request + autoLogging: true, // Automatically log HTTP requests and responses + base: clusteringEnabled === 'true' ? { pid: process.pid } : {}, // Include the process ID in the logs if clustering is enabled + customAttributeKeys: { + responseTime: 'timeSpent', // Rename the responseTime attribute to timeSpent + }, + level: logLevel || (appEnvironment === E_APP_ENVIRONMENTS.PRODUCTION ? E_APP_LOG_LEVELS.INFO : E_APP_LOG_LEVELS.TRACE), // Set the log level based on the environment and configuration + serializers: { + req(request: IncomingMessage) { + return { + method: request.method, + url: request.url, + id: request.id, + // Including the headers in the log could be in violation of privacy laws, e.g. GDPR. + // headers: request.headers, + }; + }, + res(reply: ServerResponse) { + return { + statusCode: reply.statusCode, + }; + }, + }, + transport: + appEnvironment !== E_APP_ENVIRONMENTS.PRODUCTION // Only use Pino-pretty in non-production environments + ? { + target: 'pino-pretty', + options: { + translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', + }, + } + : null, + }, + // forRoutes: ['*'], // Log all requests + }; + }, + }), + ], + controllers: [], + providers: [], + exports: [], +}) +export class LogModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + // Middleware configuration + consumer.apply(RequestLoggerMiddleware).forRoutes('*'); + } +} diff --git a/src/core/middlewares/index.ts b/src/core/middlewares/index.ts new file mode 100644 index 0000000..74d29f4 --- /dev/null +++ b/src/core/middlewares/index.ts @@ -0,0 +1 @@ +export * from './logging.middleware'; diff --git a/src/core/middlewares/logging.middleware.ts b/src/core/middlewares/logging.middleware.ts new file mode 100644 index 0000000..87aead8 --- /dev/null +++ b/src/core/middlewares/logging.middleware.ts @@ -0,0 +1,43 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; + +import { NextFunction, Request, Response } from 'express'; + +import { randomUUID } from 'node:crypto'; + +/** + * Middleware that logs incoming HTTP requests and their responses. + * + * This middleware assigns a unique request ID to each incoming request if it doesn't already have one. + * It logs a warning message if the response status code is between 400 and 500. + * + * @example + * // In your module file + * import { RequestLoggerMiddleware } from './middlewares/logging.middleware'; + * + * @Injectable() + * export class AppModule { + * configure(consumer: MiddlewareConsumer) { + * consumer + * .apply(RequestLoggerMiddleware) + * .forRoutes('*'); + * } + * } + * + * @implements {NestMiddleware} + */ +@Injectable() +export class RequestLoggerMiddleware implements NestMiddleware { + private readonly logger = new Logger(RequestLoggerMiddleware.name); + + use(req: Request, res: Response, next: NextFunction) { + req.headers['x-request-id'] = req.headers['x-request-id'] || randomUUID(); + const traceId = req.headers['x-request-id']; + res.on('finish', () => { + const { statusCode } = res; + if (statusCode >= 400 && statusCode <= 500) { + this.logger.warn(`[${traceId}}] [${req.method}] ${req.url} - ${statusCode}`); + } + }); + next(); + } +} diff --git a/src/exceptions/exceptions.constants.ts b/src/exceptions/exceptions.constants.ts deleted file mode 100644 index 5a7aa88..0000000 --- a/src/exceptions/exceptions.constants.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * This class defines constants for HTTP error codes. - */ -export class ExceptionConstants { - /** - * Constants for bad request HTTP error codes. - */ - public static readonly BadRequestCodes = { - MISSING_REQUIRED_PARAMETER: 10001, // Required parameter is missing from request - INVALID_PARAMETER_VALUE: 10002, // Parameter value is invalid - UNSUPPORTED_PARAMETER: 10003, // Request contains unsupported parameter - INVALID_CONTENT_TYPE: 10004, // Content type of request is invalid - INVALID_REQUEST_BODY: 10005, // Request body is invalid - RESOURCE_ALREADY_EXISTS: 10006, // Resource already exists - RESOURCE_NOT_FOUND: 10007, // Resource not found - REQUEST_TOO_LARGE: 10008, // Request is too large - REQUEST_ENTITY_TOO_LARGE: 10009, // Request entity is too large - REQUEST_URI_TOO_LONG: 10010, // Request URI is too long - UNSUPPORTED_MEDIA_TYPE: 10011, // Request contains unsupported media type - METHOD_NOT_ALLOWED: 10012, // Request method is not allowed - HTTP_REQUEST_TIMEOUT: 10013, // Request has timed out - VALIDATION_ERROR: 10014, // Request validation error - UNEXPECTED_ERROR: 10015, // Unexpected error occurred - INVALID_INPUT: 10016, // Invalid input - }; - - /** - * Constants for unauthorized HTTP error codes. - */ - public static readonly UnauthorizedCodes = { - UNAUTHORIZED_ACCESS: 20001, // Unauthorized access to resource - INVALID_CREDENTIALS: 20002, // Invalid credentials provided - JSON_WEB_TOKEN_ERROR: 20003, // JSON web token error - AUTHENTICATION_FAILED: 20004, // Authentication failed - ACCESS_TOKEN_EXPIRED: 20005, // Access token has expired - TOKEN_EXPIRED_ERROR: 20006, // Token has expired error - UNEXPECTED_ERROR: 20007, // Unexpected error occurred - RESOURCE_NOT_FOUND: 20008, // Resource not found - USER_NOT_VERIFIED: 20009, // User not verified - REQUIRED_RE_AUTHENTICATION: 20010, // Required re-authentication - INVALID_RESET_PASSWORD_TOKEN: 20011, // Invalid reset password token - }; - - /** - * Constants for internal server error HTTP error codes. - */ - public static readonly InternalServerErrorCodes = { - INTERNAL_SERVER_ERROR: 30001, // Internal server error - DATABASE_ERROR: 30002, // Database error - NETWORK_ERROR: 30003, // Network error - THIRD_PARTY_SERVICE_ERROR: 30004, // Third party service error - SERVER_OVERLOAD: 30005, // Server is overloaded - UNEXPECTED_ERROR: 30006, // Unexpected error occurred - }; - - /** - * Constants for forbidden HTTP error codes. - */ - public static readonly ForbiddenCodes = { - FORBIDDEN: 40001, // Access to resource is forbidden - MISSING_PERMISSIONS: 40002, // User does not have the required permissions to access the resource - EXCEEDED_RATE_LIMIT: 40003, // User has exceeded the rate limit for accessing the resource - RESOURCE_NOT_FOUND: 40004, // The requested resource could not be found - TEMPORARILY_UNAVAILABLE: 40005, // The requested resource is temporarily unavailable - }; -} diff --git a/src/exceptions/exceptions.interface.ts b/src/exceptions/exceptions.interface.ts deleted file mode 100644 index 701038d..0000000 --- a/src/exceptions/exceptions.interface.ts +++ /dev/null @@ -1,38 +0,0 @@ -export interface IException { - message: string; - code?: number; - cause?: Error; - description?: string; -} - -export interface IHttpBadRequestExceptionResponse { - code: number; - message: string; - description: string; - timestamp: string; - traceId: string; -} - -export interface IHttpInternalServerErrorExceptionResponse { - code: number; - message: string; - description: string; - timestamp: string; - traceId: string; -} - -export interface IHttpUnauthorizedExceptionResponse { - code: number; - message: string; - description: string; - timestamp: string; - traceId: string; -} - -export interface IHttpForbiddenExceptionResponse { - code: number; - message: string; - description: string; - timestamp: string; - traceId: string; -} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts deleted file mode 100644 index c8aedd9..0000000 --- a/src/exceptions/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './bad-request.exception'; -export * from './internal-server-error.exception'; -export * from './unauthorized.exception'; -export * from './forbidden.exception'; diff --git a/src/filters/all-exception.filter.ts b/src/filters/all-exception.filter.ts deleted file mode 100644 index a166aa3..0000000 --- a/src/filters/all-exception.filter.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; - -/** - * Catches all exceptions thrown by the application and sends an appropriate HTTP response. - */ -@Catch() -export class AllExceptionsFilter implements ExceptionFilter { - private readonly logger = new Logger(AllExceptionsFilter.name); - - /** - * Creates an instance of `AllExceptionsFilter`. - * - * @param {HttpAdapterHost} httpAdapterHost - the HTTP adapter host - */ - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - /** - * Catches an exception and sends an appropriate HTTP response. - * - * @param {*} exception - the exception to catch - * @param {ArgumentsHost} host - the arguments host - * @returns {void} - */ - catch(exception: any, host: ArgumentsHost): void { - // Log the exception. - this.logger.error(exception); - - // In certain situations `httpAdapter` might not be available in the - // constructor method, thus we should resolve it here. - const { httpAdapter } = this.httpAdapterHost; - - const ctx = host.switchToHttp(); - - const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; - - const request = ctx.getRequest(); - - // Construct the response body. - const responseBody = { - error: exception.code, - message: exception.message, - description: exception.description, - timestamp: new Date().toISOString(), - traceId: request.id, - }; - - // Send the HTTP response. - httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); - } -} diff --git a/src/main.ts b/src/main.ts index 736ecbf..2893dc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,63 +1,186 @@ -// Import external modules -import * as cluster from 'cluster'; -import * as os from 'os'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { ExpressAdapter, NestExpressApplication } from '@nestjs/platform-express'; + +import * as cluster from 'cluster'; + +import * as os from 'os'; + import { Logger as Pino } from 'nestjs-pino'; -// Import internal modules +import { useContainer } from 'class-validator'; + import { AppModule } from './app.module'; +import { validationOptionsUtil } from './utils'; + +import { E_APP_ENVIRONMENTS } from './config/app.config'; +import { HttpResponseInterceptor } from './core/interceptors'; // Create a logger for the bootstrap process -const logger = new Logger('bootstrap'); +const logger: Logger = new Logger('Application Bootstrap'); + +// Define a variable to store the clustering status +let clusteringEnabled: boolean = process.env.CLUSTERING && process.env.CLUSTERING === 'true' ? true : false; + +// Define the module type for hot module replacement +declare const module: any; -// Define the main function -async function bootstrap() { +/** + * Bootstrap the NestJS application. + * + * This function initializes and configures the NestJS application with various settings + * such as CORS, logging, configuration service, global prefix, validation pipes, and Swagger documentation. + * It also starts the application on the specified host and port. + * + * @returns {Promise} The initialized NestJS application instance. + * + * @remarks + * - The application uses the Pino logger. + * - Configuration values are retrieved from the ConfigService. + * - CORS is enabled for all origins. + * - The application can be configured to use a global prefix for all routes. + * - Swagger documentation can be enabled and configured. + * - Shutdown hooks are enabled for environments other than TEST. + * + * @example + * ```typescript + * async function startApp() { + * const app = await bootstrap(); + * // Additional setup or configuration can be done here + * } + * startApp(); + * ``` + */ +export async function bootstrap(): Promise { // Create the NestJS application instance - const app = await NestFactory.create(AppModule, { + const app: NestExpressApplication = await NestFactory.create(AppModule, new ExpressAdapter(), { bufferLogs: true, + forceCloseConnections: true, }); - // Use the Pino logger for the application - app.useLogger(app.get(Pino)); + // Use the application container + useContainer(app.select(AppModule), { fallbackOnErrors: true }); // Allow all origins app.enableCors(); - // Define the Swagger options and document - const options = new DocumentBuilder() - .setTitle('NestJS Starter API') - .setDescription('The API for the NestJS Starter project') - .setVersion('1.0') - .addBearerAuth() - .build(); - const document = SwaggerModule.createDocument(app, options); - - // Set up the Swagger UI endpoint - SwaggerModule.setup('docs', app, document, { - swaggerOptions: { - tagsSorter: 'alpha', - operationsSorter: 'alpha', - }, + // Use the Pino logger for the application + app.useLogger(app.get(Pino)); + + // Get configuration service from the application + const configService: ConfigService = app.get(ConfigService); + + // Get environment variables + const env: string = configService.get('app.env', { + infer: true, + }); + const port: number = configService.get('app.port', { + infer: true, + }); + const host: string = configService.get('app.host', { + infer: true, + }); + const prefixEnabled: boolean = configService.get('api.prefixEnabled', { + infer: true, + }); + const prefix: string = configService.get('api.prefix', { + infer: true, + }); + const swaggerEnabled: boolean = configService.get('swagger.swaggerEnabled', { + infer: true, }); + const swaggetTitle: string = configService.get('swagger.swaggerTitle', { + infer: true, + }); + const swaggerDescription: string = configService.get('swagger.swaggerDescription', { + infer: true, + }); + const swaggerVersion: string = configService.get('swagger.swaggerVersion', { + infer: true, + }); + const swaggerPath: string = configService.get('swagger.swaggerPath', { + infer: true, + }); + const swaggerJsonPath: string = configService.get('swagger.swaggerJsonPath', { + infer: true, + }); + const swaggerYamlPath: string = configService.get('swagger.swaggerYamlPath', { + infer: true, + }); + + // Enable the shutdown hooks + if (env !== E_APP_ENVIRONMENTS.TEST) { + app.enableShutdownHooks(); + } + + // Check if API prefix is enabled + if (prefixEnabled) { + // Set the global prefix for the application + app.setGlobalPrefix(prefix, { + exclude: ['/', '/docs', '/docs/json', '/docs/yaml'], + }); + } - // Get the configuration service from the application - const configService = app.get(ConfigService); + // Set up the global HTTP response + app.useGlobalInterceptors(new HttpResponseInterceptor()); - // Get the port number from the configuration - const PORT = configService.get('port'); + // Set up the validation pipe + app.useGlobalPipes(new ValidationPipe(validationOptionsUtil)); + + // Check if Swagger is enabled + if (swaggerEnabled) { + // Define the Swagger options and document + const options = new DocumentBuilder() + .setTitle(swaggetTitle) + .setDescription(swaggerDescription) + .setVersion(swaggerVersion) + .addServer(`http://${host}:${port}`, `${env} environment server`) + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, options); + + // Set up the Swagger UI endpoint + SwaggerModule.setup(swaggerPath, app, document, { + jsonDocumentUrl: swaggerJsonPath, + yamlDocumentUrl: swaggerYamlPath, + swaggerOptions: { + persistAuthorization: true, + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + }); + } // Start the application - await app.listen(PORT); + await app.listen(port, host, () => { + // Log the application's address and port + logger.log(`Application listening on port http://${host}:${port}`); + + // Log the application's Swagger UI endpoint + if (swaggerEnabled) { + logger.log(`Swagger UI available at http://${host}:${port}/${swaggerPath}`); + logger.log(`Swagger JSON available at http://${host}:${port}/${swaggerJsonPath}`); + logger.log(`Swagger YAML available at http://${host}:${port}/${swaggerYamlPath}`); + } + }); + + // Check if the environment is development + if (env === E_APP_ENVIRONMENTS.DEVELOPMENT) { + // Check if hot module replacement is enabled + if (module.hot) { + module.hot.accept(); + module.hot.dispose(() => app.close()); + } + } - // Log a message to indicate that the application is running - logger.log(`Application listening on port ${PORT}`); + // Return the application instance + return app; } // Check if clustering is enabled -if (process.env.CLUSTERING === 'true') { +if (clusteringEnabled) { // Get the number of CPUs on the machine const numCPUs = os.cpus().length; diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 0000000..01c38be --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default async () => { + const t = { + ["./modules/user/user.schema"]: await import("./modules/user/user.schema"), + ["./modules/user/dtos/get-profile.res.dto"]: await import("./modules/user/dtos/get-profile.res.dto"), + ["./modules/workspace/workspace.schema"]: await import("./modules/workspace/workspace.schema"), + ["./modules/auth/dtos/signup.res.dto"]: await import("./modules/auth/dtos/signup.res.dto"), + ["./modules/auth/dtos/login.res.dto"]: await import("./modules/auth/dtos/login.res.dto") + }; + return { "@nestjs/swagger": { "models": [[import("./shared/dtos/response.dto"), { "ResponseDto": { payload: { required: true }, timestamp: { required: true, type: () => String } } }], [import("./modules/user/dtos/get-profile.res.dto"), { "GetProfileResDto": { message: { required: true, type: () => String }, user: { required: true, type: () => t["./modules/user/user.schema"].User } } }], [import("./modules/workspace/dtos/workspace-req.dto"), { "CreateWorkspaceReqDto": { name: { required: true, type: () => String }, description: { required: false, type: () => String } }, "FindWorkspaceBySlugReqDto": { slug: { required: true, type: () => String } } }], [import("./modules/workspace/dtos/workspace-res.dto"), { "CreateWorkspaceResDto": { slug: { required: true, type: () => String }, name: { required: true, type: () => String }, description: { required: false, type: () => String } } }], [import("./modules/auth/dtos/signup.req.dto"), { "SignupReqDto": { email: { required: true, type: () => String }, name: { required: true, type: () => String }, password: { required: true, type: () => String }, workspaceName: { required: true, type: () => String } } }], [import("./modules/auth/dtos/signup.res.dto"), { "SignupResDto": { message: { required: true, type: () => String } } }], [import("./modules/auth/dtos/login.req.dto"), { "LoginReqDto": { email: { required: true, type: () => String }, password: { required: true, type: () => String } } }], [import("./modules/auth/dtos/login.res.dto"), { "LoginResDto": { message: { required: true, type: () => String }, accessToken: { required: true, type: () => String }, user: { required: true, type: () => t["./modules/user/user.schema"].User } } }]], "controllers": [[import("./core/application/application.controller"), { "ApplicationController": { "getHello": { type: String } } }], [import("./modules/user/user.controller"), { "UserController": { "getFullAccess": { type: t["./modules/user/dtos/get-profile.res.dto"].GetProfileResDto }, "getAllUsers": { type: [t["./modules/user/user.schema"].User] } } }], [import("./modules/workspace/workspace.controller"), { "WorkspaceController": { "create": { type: t["./modules/workspace/workspace.schema"].Workspace }, "findAll": { type: [t["./modules/workspace/workspace.schema"].Workspace] }, "findOneById": { type: t["./modules/workspace/workspace.schema"].Workspace }, "findOneBySlug": { type: t["./modules/workspace/workspace.schema"].Workspace } } }], [import("./modules/auth/auth.controller"), { "AuthController": { "signup": { type: t["./modules/auth/dtos/signup.res.dto"].SignupResDto }, "login": { type: t["./modules/auth/dtos/login.res.dto"].LoginResDto } } }]] } }; +}; \ No newline at end of file diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e50ab74..3882d0d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,22 +1,13 @@ -import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { Body, Controller, HttpCode, Post, ValidationPipe } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { Body, Controller, HttpCode, Post } from '@nestjs/common'; + +import { ApiErrorResponses } from 'src/shared'; import { AuthService } from './auth.service'; +import { Public } from './decorators'; import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; -import { BadRequestException } from '../../exceptions/bad-request.exception'; -import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; -import { UnauthorizedException } from '../../exceptions/unauthorized.exception'; - -@ApiBadRequestResponse({ - type: BadRequestException, -}) -@ApiInternalServerErrorResponse({ - type: InternalServerErrorException, -}) -@ApiUnauthorizedResponse({ - type: UnauthorizedException, -}) +@ApiErrorResponses() @ApiTags('Auth') @Controller('auth') export class AuthController { @@ -27,8 +18,9 @@ export class AuthController { type: SignupResDto, }) @HttpCode(200) + @Public() @Post('signup') - async signup(@Body(ValidationPipe) signupReqDto: SignupReqDto): Promise { + async signup(@Body() signupReqDto: SignupReqDto): Promise { return this.authService.signup(signupReqDto); } @@ -37,8 +29,9 @@ export class AuthController { type: LoginResDto, }) @HttpCode(200) + @Public() @Post('login') - async login(@Body(ValidationPipe) loginReqDto: LoginReqDto): Promise { + async login(@Body() loginReqDto: LoginReqDto): Promise { return this.authService.login(loginReqDto); } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 50f44a5..ee47d2d 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,28 +1,29 @@ // Importing the required libraries -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; -// Importing the required internal files -import { JwtUserStrategy } from './strategies/jwt-user.strategy'; +import { UserModule } from '../user/user.module'; +import { WorkspaceModule } from '../workspace/workspace.module'; -// Importing the required external modules and files import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { UserModule } from '../user/user.module'; -import { WorkspaceModule } from '../workspace/workspace.module'; +import { JwtUserStrategy } from './strategies'; @Module({ imports: [ PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.registerAsync({ inject: [ConfigService], - imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ - privateKey: configService.get('jwt.privateKey'), - publicKey: configService.get('jwt.publicKey'), - signOptions: { expiresIn: configService.get('jwt.expiresIn'), algorithm: 'RS256' }, + privateKey: configService.get('authentication.jwtPrivateKey', { + infer: true, + }), + publicKey: configService.get('authentication.jwtPublicKey', { + infer: true, + }), + signOptions: { expiresIn: configService.get('authentication.jwtExpiresIn'), algorithm: 'RS256' }, verifyOptions: { algorithms: ['RS256'], }, @@ -31,8 +32,8 @@ import { WorkspaceModule } from '../workspace/workspace.module'; UserModule, WorkspaceModule, ], - providers: [JwtUserStrategy, AuthService], controllers: [AuthController], - exports: [JwtUserStrategy, PassportModule], + providers: [ConfigService, JwtUserStrategy, AuthService], + exports: [JwtUserStrategy, PassportModule, AuthService], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index c404fca..42b3016 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,55 +1,69 @@ // External dependencies -import * as bcrypt from 'bcryptjs'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; -// Internal dependencies -import { JwtUserPayload } from './interfaces/jwt-user-payload.interface'; -import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; +import * as bcrypt from 'bcryptjs'; + +import { generateOTPCode, generateSlug } from 'src/utils'; +import { BadRequestException, UnauthorizedException } from 'src/core/exceptions'; -// Other modules dependencies -import { User } from '../user/user.schema'; import { UserQueryService } from '../user/user.query.service'; +import { User } from '../user/user.schema'; import { WorkspaceQueryService } from '../workspace/workspace.query-service'; +import { Workspace } from '../workspace/workspace.schema'; -// Shared dependencies -import { BadRequestException } from '../../exceptions/bad-request.exception'; -import { UnauthorizedException } from '../../exceptions/unauthorized.exception'; +import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; +import { JwtUserPayload } from './interfaces'; @Injectable() export class AuthService { - private readonly SALT_ROUNDS = 10; + private saltOrRounds: number; constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, private readonly userQueryService: UserQueryService, private readonly workspaceQueryService: WorkspaceQueryService, - private readonly jwtService: JwtService, - ) {} + ) { + this.saltOrRounds = this.configService.get('authentication.bcryptSaltRounds', { + infer: true, + }); + } async signup(signupReqDto: SignupReqDto): Promise { const { email, password, workspaceName, name } = signupReqDto; - const user = await this.userQueryService.findByEmail(email); + const user: User = await this.userQueryService.findByEmail(email); if (user) { throw BadRequestException.RESOURCE_ALREADY_EXISTS(`User with email ${email} already exists`); } - const workspacePayload = { - name: workspaceName, - }; - const workspace = await this.workspaceQueryService.create(workspacePayload); + // Manage workspace + const workspaceSlug: string = generateSlug(workspaceName); + const existingWorkspace: Workspace = await this.workspaceQueryService.findBySlug(workspaceSlug); + + let workspacePayload: Workspace; + if (existingWorkspace) { + workspacePayload = existingWorkspace; + } else { + workspacePayload = await this.workspaceQueryService.create({ + slug: workspaceSlug, + name: workspaceName, + }); + } + const workspace: Workspace = await this.workspaceQueryService.create(workspacePayload); // Hash password - const saltOrRounds = this.SALT_ROUNDS; - const hashedPassword = await bcrypt.hash(password, saltOrRounds); + const hashedPassword: string = await bcrypt.hash(password, this.saltOrRounds); const userPayload: User = { email, password: hashedPassword, - workspace: workspace._id, + workspace: workspace, name, - verified: true, - registerCode: this.generateCode(), + verified: false, + registerCode: generateOTPCode(), verificationCode: null, verificationCodeExpiry: null, resetToken: null, @@ -62,40 +76,34 @@ export class AuthService { }; } - /** - * Generates a random six digit OTP - * @returns {number} - returns the generated OTP - */ - generateCode(): number { - const OTP_MIN = 100000; - const OTP_MAX = 999999; - return Math.floor(Math.random() * (OTP_MAX - OTP_MIN + 1)) + OTP_MIN; - } - async login(loginReqDto: LoginReqDto): Promise { const { email, password } = loginReqDto; - const user = await this.userQueryService.findByEmail(email); + const user: User = await this.userQueryService.findByEmail(email); if (!user) { throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials'); } - const isPasswordValid = await bcrypt.compare(password, user.password); + const isPasswordValid: boolean = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials'); } + if (!user.verified) { + throw UnauthorizedException.UNAUTHORIZED_ACCESS('User is not verified'); + } + const payload: JwtUserPayload = { user: user._id, email: user.email, code: user.registerCode, }; - const accessToken = await this.jwtService.signAsync(payload); + const accessToken: string = await this.jwtService.signAsync(payload); delete user.password; return { - message: 'Login successful', + message: 'Login successfully', accessToken, user, }; diff --git a/src/modules/auth/decorators/index.ts b/src/modules/auth/decorators/index.ts new file mode 100644 index 0000000..66278fa --- /dev/null +++ b/src/modules/auth/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './get-user.decorator'; +export * from './public.decorator'; diff --git a/src/modules/auth/decorators/public.decorator.ts b/src/modules/auth/decorators/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/src/modules/auth/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/modules/auth/dtos/login.req.dto.ts b/src/modules/auth/dtos/login.req.dto.ts index 6f334c0..a87a543 100644 --- a/src/modules/auth/dtos/login.req.dto.ts +++ b/src/modules/auth/dtos/login.req.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +import { IsEmail, IsNotEmpty, IsString, IsStrongPassword, Matches, MaxLength, MinLength } from 'class-validator'; export class LoginReqDto { @ApiProperty({ description: 'Email address of the user', example: 'john@example.com' }) @@ -13,6 +14,7 @@ export class LoginReqDto { example: 'MySecurePassword!@#', }) @IsString() + @IsStrongPassword() @MinLength(8) @MaxLength(20) @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { diff --git a/src/modules/auth/dtos/signup.req.dto.ts b/src/modules/auth/dtos/signup.req.dto.ts index 71fbb89..c984242 100644 --- a/src/modules/auth/dtos/signup.req.dto.ts +++ b/src/modules/auth/dtos/signup.req.dto.ts @@ -1,5 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +import { IsEmail, IsNotEmpty, IsString, IsStrongPassword, Matches, MaxLength, MinLength } from 'class-validator'; + +import { Transform } from 'class-transformer'; export class SignupReqDto { @ApiProperty({ description: 'Email address of the user', example: 'john@example.com' }) @@ -10,6 +13,7 @@ export class SignupReqDto { @ApiProperty({ description: 'Full name of the user', example: 'John Doe' }) @IsNotEmpty() @IsString() + @Transform(({ value }) => value.trim().replace(/\s+/g, ' ')) name: string; @ApiProperty({ @@ -20,6 +24,7 @@ export class SignupReqDto { @IsString() @MinLength(8) @MaxLength(20) + @IsStrongPassword() @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { message: 'Password is too weak', }) @@ -28,5 +33,6 @@ export class SignupReqDto { @ApiProperty({ description: 'Name of the workspace', example: 'My Company' }) @IsNotEmpty() @IsString() + @Transform(({ value }) => value.trim().replace(/\s+/g, ' ')) workspaceName: string; } diff --git a/src/modules/auth/guards/index.ts b/src/modules/auth/guards/index.ts new file mode 100644 index 0000000..5d567aa --- /dev/null +++ b/src/modules/auth/guards/index.ts @@ -0,0 +1 @@ +export * from './jwt-user-auth.guard'; diff --git a/src/modules/auth/guards/jwt-user-auth.guard.ts b/src/modules/auth/guards/jwt-user-auth.guard.ts index 0a911e9..74b8970 100644 --- a/src/modules/auth/guards/jwt-user-auth.guard.ts +++ b/src/modules/auth/guards/jwt-user-auth.guard.ts @@ -1,15 +1,27 @@ import { AuthGuard } from '@nestjs/passport'; import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; -import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; +import { UnauthorizedException } from 'src/core/exceptions'; + +import { IS_PUBLIC_KEY } from '../decorators'; @Injectable() export class JwtUserAuthGuard extends AuthGuard('authUser') { + constructor(private reflector: Reflector) { + super(); + } + JSON_WEB_TOKEN_ERROR = 'JsonWebTokenError'; TOKEN_EXPIRED_ERROR = 'TokenExpiredError'; canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]); + if (isPublic) { + // 💡 See this condition + return true; + } // Add your custom authentication logic here // for example, call super.logIn(request) to establish a session. return super.canActivate(context); diff --git a/src/modules/auth/interfaces/index.ts b/src/modules/auth/interfaces/index.ts new file mode 100644 index 0000000..de0d1ef --- /dev/null +++ b/src/modules/auth/interfaces/index.ts @@ -0,0 +1 @@ +export * from './jwt-user-payload.interface'; diff --git a/src/modules/auth/strategies/index.ts b/src/modules/auth/strategies/index.ts new file mode 100644 index 0000000..f83ede2 --- /dev/null +++ b/src/modules/auth/strategies/index.ts @@ -0,0 +1 @@ +export * from './jwt-user.strategy'; diff --git a/src/modules/auth/strategies/jwt-user.strategy.ts b/src/modules/auth/strategies/jwt-user.strategy.ts index d40c9c7..ac7c217 100644 --- a/src/modules/auth/strategies/jwt-user.strategy.ts +++ b/src/modules/auth/strategies/jwt-user.strategy.ts @@ -1,10 +1,12 @@ import { ConfigService } from '@nestjs/config'; -import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { JwtUserPayload } from '../interfaces/jwt-user-payload.interface'; -import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +import { UnauthorizedException } from 'src/core/exceptions'; + +import { JwtUserPayload } from '../interfaces'; import { UserQueryService } from '../../user/user.query.service'; @Injectable() @@ -15,7 +17,8 @@ export class JwtUserStrategy extends PassportStrategy(Strategy, 'authUser') { ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: configService.get('jwt.publicKey'), + secretOrKey: configService.get('authentication.jwtPublicKey'), + ignoreExpiration: false, }); } diff --git a/src/modules/modules.module.ts b/src/modules/modules.module.ts new file mode 100644 index 0000000..f6dab2e --- /dev/null +++ b/src/modules/modules.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; + +import { AuthModule } from './auth/auth.module'; +import { UserModule } from './user/user.module'; +import { WorkspaceModule } from './workspace/workspace.module'; + +/** + * The `ModulesModule` is a NestJS module that imports and consolidates + * several other modules including `AuthModule`, `UserModule`, and `WorkspaceModule`. + * This module does not define any controllers, providers, or exports. + */ +@Module({ + imports: [AuthModule, UserModule, WorkspaceModule], + controllers: [], + providers: [], + exports: [], +}) +export class ModulesModule {} diff --git a/src/modules/user/dtos/get-profile.res.dto.ts b/src/modules/user/dtos/get-profile.res.dto.ts index 8d4537c..51c8651 100644 --- a/src/modules/user/dtos/get-profile.res.dto.ts +++ b/src/modules/user/dtos/get-profile.res.dto.ts @@ -2,6 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { User } from '../user.schema'; +/** + * Data Transfer Object for the response of getting a user profile. + */ export class GetProfileResDto { @ApiProperty({ description: 'Message to the user', diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 91b072b..81fbc4d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,24 +1,42 @@ // External dependencies import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { Controller, Get, HttpCode, Logger, UseGuards } from '@nestjs/common'; +import { Controller, Get, HttpCode, HttpStatus, Logger } from '@nestjs/common'; -// Internal dependencies -import { GetProfileResDto } from './dtos'; -import { UserDocument } from './user.schema'; +import { ApiErrorResponses } from 'src/shared'; + +import { GetUser } from '../auth/decorators'; -// Other modules dependencies -import { GetUser } from '../auth/decorators/get-user.decorator'; -import { JwtUserAuthGuard } from '../auth/guards/jwt-user-auth.guard'; +import { GetProfileResDto } from './dtos'; +import { UserQueryService } from './user.query.service'; +import { User, UserDocument } from './user.schema'; +/** + * Controller for handling user-related operations. + * + * @class UserController + * @description This controller provides endpoints for user operations such as retrieving the user's profile. + * + * @method getFullAccess + * @description Retrieves the full profile of the authenticated user. + * @param {UserDocument} user - The authenticated user's document. + * @returns {Promise} The user's profile data wrapped in a response DTO. + * + * @example + * // Example usage: + * // GET /user/me + * // Response: { message: 'Profile retrieved successfully', user: { ... } } + */ @ApiBearerAuth() +@ApiErrorResponses() @ApiTags('User') -@UseGuards(JwtUserAuthGuard) @Controller('user') export class UserController { private readonly logger = new Logger(UserController.name); + constructor(private readonly userQueryService: UserQueryService) {} + // GET /user/me - @HttpCode(200) + @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: GetProfileResDto, }) @@ -30,4 +48,14 @@ export class UserController { user, }; } + + @HttpCode(HttpStatus.OK) + @ApiOkResponse({ + type: User, + isArray: true, + }) + @Get() + async getAllUsers(): Promise { + return await this.userQueryService.findAll(); + } } diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 94d89da..59b2219 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,12 +1,21 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; +import { DatabaseCollectionNames } from 'src/shared'; + import { UserController } from './user.controller'; import { UserQueryService } from './user.query.service'; import { UserRepository } from './user.repository'; import { UserSchema } from './user.schema'; +/** + * The UserModule is a NestJS module that provides the necessary configurations + * for the user-related functionalities. It imports the MongooseModule to define + * the User schema, and it provides the UserQueryService and UserRepository for + * handling user queries and data persistence. The UserQueryService is also exported + * for use in other modules. Additionally, the UserController is included to handle + * incoming HTTP requests related to users. + */ @Module({ imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.USER, schema: UserSchema }])], providers: [UserQueryService, UserRepository], diff --git a/src/modules/user/user.query.service.ts b/src/modules/user/user.query.service.ts index dd25f94..4b79961 100644 --- a/src/modules/user/user.query.service.ts +++ b/src/modules/user/user.query.service.ts @@ -1,22 +1,38 @@ -// Objective: Implement the user query service to handle the user queries -// External dependencies import { Injectable } from '@nestjs/common'; -// Internal dependencies -import { User, UserDocument } from './user.schema'; -import { UserRepository } from './user.repository'; - -// Other modules dependencies +import { InternalServerErrorException } from 'src/core/exceptions'; +import { Identifier } from 'src/shared'; -// Shared dependencies -import { Identifier } from '../../shared/types/schema.type'; -import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; +import { UserRepository } from './user.repository'; +import { User, UserDocument } from './user.schema'; +/** + * @class UserQueryService + * @description Service to handle user-related queries. + */ @Injectable() export class UserQueryService { constructor(private readonly userRepository: UserRepository) {} - // findByEmail is a method that finds a user by their email address + /** + * Finds all users. + * @returns {Promise} - A promise that resolves to an array of user objects. + * @throws {InternalServerErrorException} - Throws an internal server error if the query fails. + */ + async findAll(): Promise { + try { + return await this.userRepository.findAllDocuments(); + } catch (error) { + throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); + } + } + + /** + * Finds a user by their email address. + * @param {string} email - The email address of the user. + * @returns {Promise} - A promise that resolves to the user object. + * @throws {InternalServerErrorException} - Throws an internal server error if the query fails. + */ async findByEmail(email: string): Promise { try { return await this.userRepository.findOne({ email }); @@ -25,7 +41,12 @@ export class UserQueryService { } } - // findById is a method that finds a user by their unique identifier + /** + * Finds a user by their unique identifier. + * @param {Identifier} id - The unique identifier of the user. + * @returns {Promise} - A promise that resolves to the user object. + * @throws {InternalServerErrorException} - Throws an internal server error if the query fails. + */ async findById(id: Identifier): Promise { try { return await this.userRepository.findById(id); @@ -34,7 +55,12 @@ export class UserQueryService { } } - // create is a method that creates a new user + /** + * Creates a new user. + * @param {User} user - The user object to be created. + * @returns {Promise} - A promise that resolves to the created user document. + * @throws {InternalServerErrorException} - Throws an internal server error if the creation fails. + */ async create(user: User): Promise { try { return await this.userRepository.create(user); diff --git a/src/modules/user/user.repository.ts b/src/modules/user/user.repository.ts index 0fdbb21..db28140 100644 --- a/src/modules/user/user.repository.ts +++ b/src/modules/user/user.repository.ts @@ -1,35 +1,71 @@ // Purpose: User repository for user module. // External dependencies -import { FilterQuery, Model, QueryOptions, Types, UpdateQuery } from 'mongoose'; import { InjectModel } from '@nestjs/mongoose'; import { Injectable } from '@nestjs/common'; -// Internal dependencies -import { User, UserDocument } from './user.schema'; +import { FilterQuery, Model, QueryOptions, Types, UpdateQuery } from 'mongoose'; -// Shared dependencies -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; +import { DatabaseCollectionNames } from 'src/shared'; +import { DatabaseAbstractRepository } from 'src/core/database/abstracts'; + +import { User, UserDocument } from './user.schema'; +/** + * UserRepository class to handle database operations for UserDocument. + */ @Injectable() -export class UserRepository { - constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model) {} +export class UserRepository extends DatabaseAbstractRepository { + /** + * Constructor to inject the user model. + * @param userModel - The user model injected by Mongoose. + */ + constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model) { + super(userModel); + } + /** + * Finds users based on the provided filter. + * @param filter - The filter query to find users. + * @returns A promise that resolves to an array of users. + */ async find(filter: FilterQuery): Promise { - return this.userModel.find(filter).lean(); + return await this.userModel.find(filter); } + /** + * Finds a user by their ID. + * @param id - The ID of the user to find. + * @returns A promise that resolves to the user or null if not found. + */ async findById(id: string | Types.ObjectId): Promise { - return this.userModel.findById(id).lean(); + return await this.userModel.findById(id); } + /** + * Finds a single user based on the provided filter. + * @param filter - The filter query to find a user. + * @returns A promise that resolves to the user or null if not found. + */ async findOne(filter: FilterQuery): Promise { - return this.userModel.findOne(filter).lean(); + return await this.userModel.findOne(filter); } + /** + * Creates a new user. + * @param user - The user data to create. + * @returns A promise that resolves to the created user document. + */ async create(user: User): Promise { return this.userModel.create(user); } + /** + * Finds a single user based on the provided filter and updates it. + * @param filter - The filter query to find a user. + * @param update - The update query to apply to the user. + * @param options - Additional query options. + * @returns A promise that resolves to the updated user document or null if not found. + */ async findOneAndUpdate( filter: FilterQuery, update: UpdateQuery, @@ -38,7 +74,18 @@ export class UserRepository { return this.userModel.findOneAndUpdate(filter, update, options); } - async findByIdAndUpdate(id, update: UpdateQuery, options: QueryOptions): Promise { + /** + * Finds a user by their ID and updates it. + * @param id - The ID of the user to update. + * @param update - The update query to apply to the user. + * @param options - Additional query options. + * @returns A promise that resolves to the updated user document or null if not found. + */ + async findByIdAndUpdate( + id: string | Types.ObjectId, + update: UpdateQuery, + options: QueryOptions, + ): Promise { return this.userModel.findByIdAndUpdate(id, update, options); } } diff --git a/src/modules/user/user.schema.ts b/src/modules/user/user.schema.ts index 4a37044..0a61072 100644 --- a/src/modules/user/user.schema.ts +++ b/src/modules/user/user.schema.ts @@ -1,34 +1,48 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose'; -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { DatabaseCollectionNames } from '../../shared/enums'; -import { Identifier } from '../../shared/types'; +import { DatabaseCollectionNames } from 'src/shared'; +import { EntityDocumentHelper } from 'src/utils'; +import { getDatabaseSchemaOptions } from 'src/core/database/database-schema-options'; -@Schema({ - timestamps: true, - collection: DatabaseCollectionNames.USER, -}) -export class User { - // _id is the unique identifier of the user - @ApiProperty({ - description: 'The unique identifier of the user', - example: '643405452324db8c464c0584', - }) - @Prop({ - type: MongooseSchema.Types.ObjectId, - default: () => new Types.ObjectId(), - }) - _id?: Types.ObjectId; +import { Workspace } from '../workspace/workspace.schema'; + +export type UserDocument = HydratedDocument; +/** + * Represents a User entity in the database. + * + * @remarks + * This class extends `EntityDocumentHelper` and is decorated with the `@Schema` decorator + * to define the schema options for the User collection. + * + * @property {string} email - The unique identifier of the user. + * @property {string} [password] - The hashed password of the user. + * @property {Workspace} workspace - The unique identifier of the workspace that the user belongs to. + * @property {string} [name] - The full name of the user. + * @property {boolean} verified - Indicates whether the user has verified their email address. + * @property {number} [verificationCode] - A 6-digit number sent to the user's email address to verify their email address. + * @property {Date} [verificationCodeExpiry] - The date and time when the verification code expires. + * @property {string} [resetToken] - Token used for resetting the user's password. + * @property {number} [registerCode] - Code used when the user is going to reset or change their password, causing all active sessions to be logged out. + */ +@Schema(getDatabaseSchemaOptions(DatabaseCollectionNames.USER, ['password'])) +export class User extends EntityDocumentHelper { // email is the unique identifier of the user @ApiProperty({ + type: String, description: 'The unique identifier of the user', example: 'john@example.com', }) @Prop({ + type: String, required: true, + unique: true, + index: true, + lowercase: true, + trim: true, }) email: string; @@ -45,8 +59,10 @@ export class User { @Prop({ type: MongooseSchema.Types.ObjectId, ref: DatabaseCollectionNames.WORKSPACE, + required: true, + autopopulate: true, }) - workspace: Identifier; + workspace: Workspace; // name is the full name of the user @ApiProperty({ @@ -62,7 +78,7 @@ export class User { example: true, }) @Prop({ - type: MongooseSchema.Types.Boolean, + type: Boolean, default: false, }) verified: boolean; @@ -70,14 +86,14 @@ export class User { // verificationCode is a 6-digit number that is sent to the user's email address to verify their email address @ApiHideProperty() @Prop({ - type: MongooseSchema.Types.Number, + type: Number, }) verificationCode?: number; // verificationCodeExpiry is the date and time when the verification code expires @ApiHideProperty() @Prop({ - type: MongooseSchema.Types.Date, + type: Date, }) verificationCodeExpiry?: Date; @@ -88,26 +104,11 @@ export class User { // registerCode is used for when user is going to reset password or change password perform at time all same user login session will be logout @ApiHideProperty() @Prop({ - type: MongooseSchema.Types.Number, + type: Number, }) registerCode?: number; - - @ApiProperty({ - description: 'Date of creation', - }) - @Prop() - createdAt?: Date; - - @ApiProperty({ - description: 'Date of last update', - }) - @Prop() - updatedAt?: Date; } -export type UserIdentifier = Identifier | User; - -export type UserDocument = HydratedDocument; export const UserSchema = SchemaFactory.createForClass(User); UserSchema.index({ email: 1, isActive: 1 }); diff --git a/src/modules/workspace/dtos/index.ts b/src/modules/workspace/dtos/index.ts new file mode 100644 index 0000000..4725993 --- /dev/null +++ b/src/modules/workspace/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './workspace-req.dto'; +export * from './workspace-res.dto'; diff --git a/src/modules/workspace/dtos/workspace-req.dto.ts b/src/modules/workspace/dtos/workspace-req.dto.ts new file mode 100644 index 0000000..539d826 --- /dev/null +++ b/src/modules/workspace/dtos/workspace-req.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +import { Transform } from 'class-transformer'; + +import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; + +import { generateSlug } from 'src/utils'; + +export class CreateWorkspaceReqDto { + @ApiProperty({ + type: String, + description: 'Name of the workspace', + example: 'My Workspace', + }) + @IsString() + @IsNotEmpty() + @MinLength(5) + @MaxLength(120) + @Transform(({ value }) => value.trim().replace(/\s\s+/g, ' ').toUppercCase()) + name: string; + + @ApiPropertyOptional({ + type: String, + description: 'Description of the workspace', + example: 'My workspace description', + }) + description?: string; +} + +export class FindWorkspaceBySlugReqDto { + @ApiProperty({ + type: String, + description: 'The slug of the workspace', + example: 'my-workspace', + }) + @IsString() + @IsNotEmpty() + @Transform(({ value }) => generateSlug(value)) + slug: string; +} diff --git a/src/modules/workspace/dtos/workspace-res.dto.ts b/src/modules/workspace/dtos/workspace-res.dto.ts new file mode 100644 index 0000000..deb5418 --- /dev/null +++ b/src/modules/workspace/dtos/workspace-res.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +import { Transform } from 'class-transformer'; + +import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateWorkspaceResDto { + @ApiProperty({ + type: String, + description: 'The slug of the workspace', + example: 'my-workspace', + }) + @IsString() + @IsNotEmpty() + slug: string; + + @ApiProperty({ + type: String, + description: 'Name of the workspace', + example: 'My Workspace', + }) + @IsString() + @IsNotEmpty() + @MinLength(5) + @MaxLength(120) + @Transform(({ value }) => value.trim().replace(/\s\s+/g, ' ').toUppercCase()) + name: string; + + @ApiPropertyOptional({ + type: String, + description: 'Description of the workspace', + example: 'My workspace description', + }) + description?: string; +} diff --git a/src/modules/workspace/workspace.controller.ts b/src/modules/workspace/workspace.controller.ts new file mode 100644 index 0000000..b64718b --- /dev/null +++ b/src/modules/workspace/workspace.controller.ts @@ -0,0 +1,67 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; +import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; + +import { generateSlug } from 'src/utils'; +import { ApiErrorResponses, ApiGlobalResponse } from 'src/shared'; + +import { CreateWorkspaceReqDto, FindWorkspaceBySlugReqDto } from './dtos'; +import { WorkspaceQueryService } from './workspace.query-service'; +import { Workspace } from './workspace.schema'; + +@ApiBearerAuth() +@ApiTags('Workspaces') +@Controller('workspaces') +export class WorkspaceController { + constructor(private readonly workspaceQueryService: WorkspaceQueryService) {} + + @HttpCode(HttpStatus.CREATED) + @ApiGlobalResponse(Workspace, { + description: 'The workspace has been successfully created.', + isCreatedResponse: true, + }) + @Post() + async create(@Body() createWorkspaceReqDto: CreateWorkspaceReqDto): Promise { + const workspaceSlug: string = generateSlug(createWorkspaceReqDto.name); + + const workspace: Workspace = { + slug: workspaceSlug, + name: createWorkspaceReqDto.name, + description: createWorkspaceReqDto.description, + }; + + const existingWorkspace: Workspace = await this.workspaceQueryService.findBySlug(workspaceSlug); + if (existingWorkspace) { + return existingWorkspace; + } + + return await this.workspaceQueryService.create(workspace); + } + + @HttpCode(HttpStatus.OK) + @ApiGlobalResponse(Workspace, { + description: 'The workspaces have been successfully retrieved.', + isArray: true, + }) + @Get() + async findAll(): Promise { + return await this.workspaceQueryService.findAllWorkspaces(); + } + + @HttpCode(HttpStatus.OK) + @ApiGlobalResponse(Workspace, { + description: 'The workspace has been successfully retrieved.', + }) + @Get(':id') + async findOneById(@Param('id') id: string): Promise { + return await this.workspaceQueryService.findById(id); + } + + @HttpCode(HttpStatus.OK) + @ApiOkResponse({ + type: Workspace, + }) + @Get('slug/:slug') + async findOneBySlug(@Param() slug: FindWorkspaceBySlugReqDto): Promise { + return await this.workspaceQueryService.findBySlug(slug.slug); + } +} diff --git a/src/modules/workspace/workspace.module.ts b/src/modules/workspace/workspace.module.ts index ddeb16d..d59c690 100644 --- a/src/modules/workspace/workspace.module.ts +++ b/src/modules/workspace/workspace.module.ts @@ -1,13 +1,39 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; +import { DatabaseCollectionNames } from 'src/shared'; + +import { WorkspaceController } from './workspace.controller'; import { WorkspaceQueryService } from './workspace.query-service'; import { WorkspaceRepository } from './workspace.repository'; import { WorkspaceSchema } from './workspace.schema'; +/** + * The WorkspaceModule is responsible for managing the workspace-related functionalities. + * + * @module WorkspaceModule + * + * @description + * This module imports the MongooseModule to define the Workspace schema and provides + * the WorkspaceQueryService and WorkspaceRepository for handling workspace data operations. + * It also exports the WorkspaceQueryService for use in other modules. + * + * @imports + * - MongooseModule: Registers the Workspace schema with Mongoose. + * + * @controllers + * - WorkspaceController: Controller for handling workspace operations. + * + * @providers + * - WorkspaceQueryService: Service for querying workspace data. + * - WorkspaceRepository: Repository for workspace data operations. + * + * @exports + * - WorkspaceQueryService: Makes the WorkspaceQueryService available to other modules. + */ @Module({ imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.WORKSPACE, schema: WorkspaceSchema }])], + controllers: [WorkspaceController], providers: [WorkspaceQueryService, WorkspaceRepository], exports: [WorkspaceQueryService], }) diff --git a/src/modules/workspace/workspace.query-service.ts b/src/modules/workspace/workspace.query-service.ts index cc659a3..96ded4f 100644 --- a/src/modules/workspace/workspace.query-service.ts +++ b/src/modules/workspace/workspace.query-service.ts @@ -1,14 +1,29 @@ import { Injectable } from '@nestjs/common'; -import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; +import { InternalServerErrorException } from 'src/core/exceptions'; -import { Workspace } from './workspace.schema'; import { WorkspaceRepository } from './workspace.repository'; +import { Workspace } from './workspace.schema'; +/** + * Service to handle workspace queries. + * + * @class WorkspaceQueryService + * @constructor + * @param {WorkspaceRepository} workspaceRepository - The repository to manage workspace data. + */ @Injectable() export class WorkspaceQueryService { constructor(private readonly workspaceRepository: WorkspaceRepository) {} + /** + * Creates a new workspace. + * + * @method create + * @param {Workspace} workspace - The workspace entity to be created. + * @returns {Promise} The created workspace entity. + * @throws {InternalServerErrorException} If an error occurs during creation. + */ async create(workspace: Workspace): Promise { try { return await this.workspaceRepository.create(workspace); @@ -17,6 +32,29 @@ export class WorkspaceQueryService { } } + /** + * Finds all workspaces. + * + * @method findAllWorkspaces + * @returns {Promise} An array of workspace entities. + * @throws {InternalServerErrorException} If an error occurs during the search. + */ + async findAllWorkspaces(): Promise { + try { + return await this.workspaceRepository.findAllDocuments(); + } catch (error) { + throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); + } + } + + /** + * Finds a workspace by its ID. + * + * @method findById + * @param {string} workspaceId - The ID of the workspace to find. + * @returns {Promise} The found workspace entity. + * @throws {InternalServerErrorException} If an error occurs during the search. + */ async findById(workspaceId: string): Promise { try { return await this.workspaceRepository.findById(workspaceId); @@ -24,4 +62,20 @@ export class WorkspaceQueryService { throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); } } + + /** + * Finds a workspace by its slug. + * + * @method findBySlug + * @param {string} slug - The slug of the workspace to find. + * @returns {Promise} The found workspace entity. + * @throws {InternalServerErrorException} If an error occurs during the search. + */ + async findBySlug(slug: string): Promise { + try { + return await this.workspaceRepository.findBySlug(slug); + } catch (error) { + throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); + } + } } diff --git a/src/modules/workspace/workspace.repository.ts b/src/modules/workspace/workspace.repository.ts index c18a809..c065b75 100644 --- a/src/modules/workspace/workspace.repository.ts +++ b/src/modules/workspace/workspace.repository.ts @@ -1,39 +1,98 @@ -import { FilterQuery, Model, ProjectionType, QueryOptions, UpdateQuery } from 'mongoose'; import { InjectModel } from '@nestjs/mongoose'; import { Injectable } from '@nestjs/common'; -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; +import { FilterQuery, Model, ProjectionType, QueryOptions, UpdateQuery } from 'mongoose'; + +import { DatabaseCollectionNames } from 'src/shared'; +import { DatabaseAbstractRepository } from 'src/core/database/abstracts'; + import { Workspace, WorkspaceDocument } from './workspace.schema'; @Injectable() -export class WorkspaceRepository { - constructor(@InjectModel(DatabaseCollectionNames.WORKSPACE) private workspaceModel: Model) {} +export class WorkspaceRepository extends DatabaseAbstractRepository { + /** + * Creates an instance of WorkspaceRepository. + * + * @param workspaceModel - The Mongoose model for Workspace documents. + */ + constructor(@InjectModel(DatabaseCollectionNames.WORKSPACE) private workspaceModel: Model) { + super(workspaceModel); + } - async find(filter: FilterQuery, selectOptions?: ProjectionType): Promise { - return this.workspaceModel.find(filter, selectOptions).lean(); + /** + * Finds multiple Workspace documents based on the provided filter. + * + * @param filter - The filter query to match documents. + * @param selectOptions - Optional projection options to select specific fields. + * @returns A promise that resolves to an array of Workspace documents. + */ + async find(filter: FilterQuery, selectOptions?: ProjectionType): Promise { + return this.workspaceModel.find(filter, selectOptions); } - async findOne(filter: FilterQuery): Promise { - return this.workspaceModel.findOne(filter).lean(); + /** + * Finds a single Workspace document based on the provided filter. + * + * @param filter - The filter query to match the document. + * @returns A promise that resolves to a Workspace document. + */ + async findOne(filter: FilterQuery): Promise { + return this.workspaceModel.findOne(filter); } - async create(workspace: Workspace): Promise { + /** + * Creates a new Workspace document. + * + * @param workspace - The Workspace data to create the document. + * @returns A promise that resolves to the created Workspace document. + */ + async create(workspace: Workspace): Promise { return this.workspaceModel.create(workspace); } - async findById(workspaceId: string): Promise { - return this.workspaceModel.findById(workspaceId).lean(); + /** + * Finds a Workspace document by its ID. + * + * @param workspaceId - The ID of the Workspace document to find. + * @returns A promise that resolves to the found Workspace document. + */ + async findById(workspaceId: string): Promise { + return this.workspaceModel.findById(workspaceId); + } + + /** + * Finds a Workspace document by its slug. + * + * @param slug - The slug of the Workspace document to find. + * @returns A promise that resolves to the found Workspace document. + */ + async findBySlug(slug: string): Promise { + return this.findOne({ slug }); } + /** + * Finds a single Workspace document based on the provided filter and updates it. + * + * @param filter - The filter query to match the document. + * @param update - The update query to apply to the document. + * @param options - Optional query options. + * @returns A promise that resolves to the updated Workspace document. + */ async findOneAndUpdate( filter: FilterQuery, update: UpdateQuery, options?: QueryOptions, ): Promise { - return this.workspaceModel.findOneAndUpdate(filter, update, options).lean(); + return this.workspaceModel.findOneAndUpdate(filter, update, options); } - async findByIdAndDelete(workspaceId: string): Promise { - return this.workspaceModel.findByIdAndDelete(workspaceId).lean(); + /** + * Finds a Workspace document by its ID and deletes it. + * + * @param workspaceId - The ID of the Workspace document to delete. + * @returns A promise that resolves to the deleted Workspace document. + */ + async findByIdAndDelete(workspaceId: string): Promise { + return this.workspaceModel.findByIdAndDelete(workspaceId); } } diff --git a/src/modules/workspace/workspace.schema.ts b/src/modules/workspace/workspace.schema.ts index 251b9d5..52db074 100644 --- a/src/modules/workspace/workspace.schema.ts +++ b/src/modules/workspace/workspace.schema.ts @@ -1,49 +1,67 @@ import { ApiProperty } from '@nestjs/swagger'; -import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; -import { Identifier } from '../../shared/types/schema.type'; +import { HydratedDocument } from 'mongoose'; -@Schema({ - timestamps: true, - collection: DatabaseCollectionNames.WORKSPACE, -}) -export class Workspace { +import { DatabaseCollectionNames } from 'src/shared'; +import { getDatabaseSchemaOptions } from 'src/core/database/database-schema-options'; +import { EntityDocumentHelper } from 'src/utils'; + +export type WorkspaceDocument = HydratedDocument; + +/** + * Represents a workspace entity in the database. + * + * @extends EntityDocumentHelper + * + * @property {string} name - The name of the workspace. + * @property {string} [description] - The description of the workspace. + * + * @example + * const workspace = new Workspace(); + * workspace.name = 'My Workspace'; + * workspace.description = 'This is my workspace'; + */ +@Schema(getDatabaseSchemaOptions(DatabaseCollectionNames.WORKSPACE)) +export class Workspace extends EntityDocumentHelper { @ApiProperty({ - description: 'The unique identifier of the workspace', - example: '507f191e810c19729de860ea', + type: String, + description: 'The slug of the workspace', + example: 'my-workspace', }) @Prop({ - type: MongooseSchema.Types.ObjectId, - default: () => new Types.ObjectId(), + type: String, + required: true, + unique: true, + lowercase: true, }) - _id?: Types.ObjectId; + slug: string; @ApiProperty({ + type: String, description: 'The name of the workspace', example: 'My Workspace', }) @Prop({ - type: MongooseSchema.Types.String, + type: String, required: true, + uppercase: true, }) name: string; @ApiProperty({ - description: 'Date of creation', + type: String, + description: 'The description of the workspace', + example: 'This is my workspace', }) - @Prop() - createdAt?: Date; - - @ApiProperty({ - description: 'Date of last update', + @Prop({ + type: String, + required: false, + default: '', }) - @Prop() - updatedAt?: Date; + description?: string; } -export type WorkspaceIdentifier = Identifier | Workspace; - -export type WorkspaceDocument = HydratedDocument; export const WorkspaceSchema = SchemaFactory.createForClass(Workspace); + +WorkspaceSchema.index({ name: 1 }, { unique: true }); diff --git a/src/shared/decorators/api-error-responses.decorator.ts b/src/shared/decorators/api-error-responses.decorator.ts new file mode 100644 index 0000000..1569af2 --- /dev/null +++ b/src/shared/decorators/api-error-responses.decorator.ts @@ -0,0 +1,53 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiGatewayTimeoutResponse, + ApiInternalServerErrorResponse, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; + +import { + BadRequestException, + ForbiddenException, + GatewayTimeoutException, + InternalServerErrorException, + UnauthorizedException, +} from 'src/core/exceptions'; + +/** + * A decorator that applies multiple API error response decorators to a route handler. + * + * This decorator includes the following responses: + * - `400 Bad Request`: Indicates that the server cannot process the request due to client error. + * - `403 Forbidden`: Indicates that the client does not have access rights to the content. + * - `504 Gateway Timeout`: Indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server. + * - `500 Internal Server Error`: Indicates that the server encountered an unexpected condition that prevented it from fulfilling the request. + * - `401 Unauthorized`: Indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. + * + * @returns A function that applies the specified decorators. + */ +export const ApiErrorResponses = () => { + return applyDecorators( + ApiBadRequestResponse({ + description: 'Bad Request', + type: BadRequestException, + }), + ApiForbiddenResponse({ + description: 'Forbidden', + type: ForbiddenException, + }), + ApiGatewayTimeoutResponse({ + description: 'Gateway Timeout', + type: GatewayTimeoutException, + }), + ApiInternalServerErrorResponse({ + description: 'Internal Server Error', + type: InternalServerErrorException, + }), + ApiUnauthorizedResponse({ + description: 'Unauthorized', + type: UnauthorizedException, + }), + ); +}; diff --git a/src/shared/decorators/api-global-response.decorator.ts b/src/shared/decorators/api-global-response.decorator.ts new file mode 100644 index 0000000..98df087 --- /dev/null +++ b/src/shared/decorators/api-global-response.decorator.ts @@ -0,0 +1,89 @@ +import { applyDecorators, NotFoundException, Type } from '@nestjs/common'; +import { + ApiOkResponse, + ApiCreatedResponse, + ApiExtraModels, + getSchemaPath, + ApiResponseOptions, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiGatewayTimeoutResponse, + ApiInternalServerErrorResponse, + ApiUnauthorizedResponse, + ApiNotFoundResponse, +} from '@nestjs/swagger'; + +import { + BadRequestException, + ForbiddenException, + GatewayTimeoutException, + InternalServerErrorException, + UnauthorizedException, +} from 'src/core/exceptions'; + +import { ResponseDto } from '../dtos'; + +export const ApiGlobalResponse = >( + model: TModel, + { + description, + isArray = false, + isCreatedResponse = false, + }: { + description?: string; + isArray?: boolean; + isCreatedResponse?: boolean; + }, +) => { + const schemaDescription = description || 'The resource has been successfully retrieved.'; + + const apiResponseOptions: ApiResponseOptions = { + description: schemaDescription, + schema: { + allOf: [ + { $ref: getSchemaPath(ResponseDto) }, + { + properties: { + payload: { + type: isArray ? 'array' : 'object', + items: isArray ? { $ref: getSchemaPath(model) } : undefined, + $ref: isArray ? undefined : getSchemaPath(model), + }, + timestamp: { + type: 'string', + }, + }, + }, + ], + }, + }; + + return applyDecorators( + ApiExtraModels(ResponseDto, model), + isCreatedResponse ? ApiCreatedResponse(apiResponseOptions) : ApiOkResponse(apiResponseOptions), + ApiNotFoundResponse({ + description: 'Not Found', + type: NotFoundException, + }), + ApiBadRequestResponse({ + description: 'Bad Request', + type: BadRequestException, + }), + ApiForbiddenResponse({ + description: 'Forbidden', + type: ForbiddenException, + }), + ApiGatewayTimeoutResponse({ + description: 'Gateway Timeout', + type: GatewayTimeoutException, + }), + ApiInternalServerErrorResponse({ + description: 'Internal Server Error', + type: InternalServerErrorException, + }), + ApiUnauthorizedResponse({ + description: 'Unauthorized', + type: UnauthorizedException, + }), + ); +}; diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..0cf2513 --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './api-error-responses.decorator'; +export * from './api-global-response.decorator'; diff --git a/src/shared/dtos/index.ts b/src/shared/dtos/index.ts new file mode 100644 index 0000000..b0baf90 --- /dev/null +++ b/src/shared/dtos/index.ts @@ -0,0 +1 @@ +export * from './response.dto'; diff --git a/src/shared/dtos/response.dto.ts b/src/shared/dtos/response.dto.ts new file mode 100644 index 0000000..213838e --- /dev/null +++ b/src/shared/dtos/response.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * Dto for the response + */ +export class ResponseDto { + @ApiProperty() + payload: T; + @ApiProperty({ example: '2021-09-01T00:00:00.000Z' }) + timestamp: string; +} diff --git a/src/shared/enums/db.enum.ts b/src/shared/enums/db.enum.ts index 536fd13..75f4b4c 100644 --- a/src/shared/enums/db.enum.ts +++ b/src/shared/enums/db.enum.ts @@ -1,3 +1,10 @@ +/** + * Enum representing the names of collections in the database. + * + * @enum {string} + * @property {string} USER - Represents the 'users' collection. + * @property {string} WORKSPACE - Represents the 'workspaces' collection. + */ export enum DatabaseCollectionNames { USER = 'users', WORKSPACE = 'workspaces', diff --git a/src/shared/enums/index.ts b/src/shared/enums/index.ts index 6d0fcb2..3df766d 100644 --- a/src/shared/enums/index.ts +++ b/src/shared/enums/index.ts @@ -1,3 +1 @@ -export * from './log-level.enum'; -export * from './node-env.enum'; export * from './db.enum'; diff --git a/src/shared/enums/log-level.enum.ts b/src/shared/enums/log-level.enum.ts deleted file mode 100644 index e8ee459..0000000 --- a/src/shared/enums/log-level.enum.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum LogLevel { - SILENT = 'silent', - TRACE = 'trace', - DEBUG = 'debug', - INFO = 'info', - WARN = 'warn', - ERROR = 'error', - FATAL = 'fatal', -} diff --git a/src/shared/enums/node-env.enum.ts b/src/shared/enums/node-env.enum.ts deleted file mode 100644 index 1d238c2..0000000 --- a/src/shared/enums/node-env.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum NodeEnv { - DEVELOPMENT = 'development', - TEST = 'test', - PRODUCTION = 'production', -} diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..1571487 --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,4 @@ +export * from './decorators'; +export * from './dtos'; +export * from './enums'; +export * from './types'; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 663ebcf..1a377b6 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1 +1,2 @@ +export * from './or-never.type'; export * from './schema.type'; diff --git a/src/shared/types/or-never.type.ts b/src/shared/types/or-never.type.ts new file mode 100644 index 0000000..4df7f38 --- /dev/null +++ b/src/shared/types/or-never.type.ts @@ -0,0 +1,27 @@ +/** + * A utility type that represents a type `T` or `never`. + * + * This type can be useful in scenarios where you want to explicitly + * indicate that a type can either be a specific type `T` or `never`. + * + * @template T - The type to be used or `never`. + * + * @example + * // Example usage of OrNeverType + * + * // A type that can be a string or never + * type StringOrNever = OrNeverType; + * + * // A function that accepts a parameter of type StringOrNever + * function exampleFunction(param: StringOrNever) { + * if (typeof param === 'string') { + * console.log(`The parameter is a string: ${param}`); + * } else { + * console.log('The parameter is never'); + * } + * } + * + * exampleFunction('Hello, world!'); // The parameter is a string: Hello, world! + * exampleFunction(undefined as never); // The parameter is never + */ +export type OrNeverType = T | never; diff --git a/src/shared/types/schema.type.ts b/src/shared/types/schema.type.ts index b83d8e9..e992cf5 100644 --- a/src/shared/types/schema.type.ts +++ b/src/shared/types/schema.type.ts @@ -1,3 +1,13 @@ import { Types } from 'mongoose'; +/** + * @type {Identifier} + * + * Represents a unique identifier which can be either a MongoDB ObjectId or a string. + * This type is used to define entities that can be identified by either type of identifier. + * + * @example + * const id1: Identifier = new Types.ObjectId(); // MongoDB ObjectId + * const id2: Identifier = 'some-string-id'; // String identifier + */ export type Identifier = Types.ObjectId | string; diff --git a/src/utils/date-time.util.ts b/src/utils/date-time.util.ts new file mode 100644 index 0000000..0795fe8 --- /dev/null +++ b/src/utils/date-time.util.ts @@ -0,0 +1,30 @@ +/** + * Converts a time string to milliseconds. + * + * The time string should end with a single character representing the time unit: + * - 'd' for days + * - 'h' for hours + * - 'm' for minutes + * - 's' for seconds + * + * @param {string} time - The time string to convert. + * @returns {number} The equivalent time in milliseconds. + * @throws {Error} If the time format is invalid. + */ +export function convertToMilliseconds(time: string): number { + const timeUnit = time.slice(-1); // Obtiene el último carácter (d, h, m, s) + const timeValue = parseInt(time.slice(0, -1)); // Obtiene el valor numérico + + switch (timeUnit) { + case 'd': // días + return timeValue * 24 * 60 * 60 * 1000; + case 'h': // horas + return timeValue * 60 * 60 * 1000; + case 'm': // minutos + return timeValue * 60 * 1000; + case 's': // segundos + return timeValue * 1000; + default: + throw new Error(`Invalid time format: ${time}`); + } +} diff --git a/src/utils/deep-resolver.util.ts b/src/utils/deep-resolver.util.ts new file mode 100644 index 0000000..f59a2f6 --- /dev/null +++ b/src/utils/deep-resolver.util.ts @@ -0,0 +1,49 @@ +/** + * Recursively resolves all promises within an input object, array, or value. + * + * @param input - The input value which can be a promise, array, object, or any other type. + * @returns A promise that resolves to the same structure as the input, but with all promises resolved. + * + * @example + * ```typescript + * const input = { + * a: Promise.resolve(1), + * b: [Promise.resolve(2), 3], + * c: { + * d: Promise.resolve(4), + * e: 5 + * } + * }; + * + * const result = await deepResolvePromisesUtil(input); + * // result will be: { a: 1, b: [2, 3], c: { d: 4, e: 5 } } + * ``` + */ +export const deepResolvePromisesUtil = async (input) => { + if (input instanceof Promise) { + return await input; + } + + if (Array.isArray(input)) { + const resolvedArray = await Promise.all(input.map(deepResolvePromisesUtil)); + return resolvedArray; + } + + if (input instanceof Date) { + return input; + } + + if (typeof input === 'object' && input !== null) { + const keys = Object.keys(input); + const resolvedObject = {}; + + for (const key of keys) { + const resolvedValue = await deepResolvePromisesUtil(input[key]); + resolvedObject[key] = resolvedValue; + } + + return resolvedObject; + } + + return input; +}; diff --git a/src/utils/document-entity-helper.util.ts b/src/utils/document-entity-helper.util.ts new file mode 100644 index 0000000..7e1b48e --- /dev/null +++ b/src/utils/document-entity-helper.util.ts @@ -0,0 +1,53 @@ +import { Prop } from '@nestjs/mongoose'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * Abstract class representing a helper for entity documents. + * + * @template T - The type of the entity. + * + * @property {string} [_id] - The unique identifier of the entity. + * @property {Date} [createdAt] - Date of creation. + * @property {Date} [updatedAt] - Date of last update. + * + * @param {Partial} [partial] - Partial entity to initialize the helper with. + */ +export abstract class EntityDocumentHelper { + @ApiPropertyOptional({ + type: String, + description: 'The unique identifier of the entity', + example: '643405452324db8c464c0584', + required: false, + }) + public _id?: string; + + @ApiPropertyOptional({ + type: Date, + description: 'Date of creation', + example: '2021-08-01T00:00:00.000Z', + required: true, + }) + @Prop({ + type: Date, + default: new Date(), + }) + public createdAt?: Date; + + @ApiPropertyOptional({ + type: Date, + description: 'Date of last update', + example: '2021-08-01T00:00:00.000Z', + required: false, + }) + @Prop({ + type: Date, + default: new Date(), + }) + public updatedAt?: Date; + + constructor(partial?: Partial) { + if (partial) { + Object.assign(this, partial); + } + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e82d24f --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from './date-time.util'; +export * from './deep-resolver.util'; +export * from './document-entity-helper.util'; +export * from './number.util'; +export * from './string.util'; +export * from './validate-config.util'; +export * from './validation-options.util'; diff --git a/src/utils/number.util.ts b/src/utils/number.util.ts new file mode 100644 index 0000000..04ab054 --- /dev/null +++ b/src/utils/number.util.ts @@ -0,0 +1,47 @@ +/** + * Generates a random OTP code within a specified range. + * + * If any of the parameters are not provided, they will be defaulted to 100000 and 999999. + * + * The generated OTP code will be a whole number between the minimum and maximum values (inclusive). + * + * @param {number} [min=100000] - The minimum value of the OTP range. + * @param {number} [max=999999] - The maximum value of the OTP range. + * @return {number} The randomly generated OTP code. + * @throws {Error} If the minimum value is greater than the maximum value. + * @throws {Error} If the minimum or maximum values are negative. + * @throws {Error} If the minimum or maximum values are not six digits long. + * @throws {Error} If the minimum and maximum values are the same. + * @throws {Error} If the minimum or maximum values are greater than 999999. + * @throws {Error} If the minimum or maximum values are less than 100000. + * @throws {Error} If the minimum or maximum values are not numbers. + * @throws {Error} If the minimum or maximum values are not whole numbers. + */ +export const generateOTPCode = (min?: number, max?: number): number => { + min = min || 100000; + max = max || 999999; + + if (min > max) { + throw new Error('Minimum value cannot be greater than maximum value'); + } else if (min < 0 || max < 0) { + throw new Error('Minimum and maximum values cannot be negative'); + } else if (min.toString().length !== 6 || max.toString().length !== 6) { + throw new Error('Minimum and maximum values must be six digits long'); + } else if (min === max) { + throw new Error('Minimum and maximum values cannot be the same'); + } else if (min > 999999 || max > 999999) { + throw new Error('Minimum and maximum values cannot be greater than 999999'); + } else if (max < 100000 || min < 100000) { + throw new Error('Minimum and maximum values cannot be less than 100000'); + } else if (typeof min !== 'number' || typeof max !== 'number') { + throw new Error('Minimum and maximum values must be numbers'); + } else if (min % 1 !== 0 || max % 1 !== 0) { + throw new Error('Minimum and maximum values must be whole numbers'); + } else if (min > 999999 || max > 999999) { + throw new Error('Minimum and maximum values cannot be greater than 999999'); + } else if (min % 1 !== 0 || max % 1 !== 0) { + throw new Error('Minimum and maximum values must be whole numbers'); + } else { + return Math.floor(Math.random() * (max - min + 1)) + min; + } +}; diff --git a/src/utils/string.util.ts b/src/utils/string.util.ts new file mode 100644 index 0000000..275984d --- /dev/null +++ b/src/utils/string.util.ts @@ -0,0 +1,13 @@ +/** + * Generate a slug from a text + * @param {string} text - The text to generate a slug from + * @returns {string} The generated slug + */ +export const generateSlug = (text: string): string => { + return text + .trim() + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); +}; diff --git a/src/utils/validate-config.util.ts b/src/utils/validate-config.util.ts new file mode 100644 index 0000000..ec6f0d5 --- /dev/null +++ b/src/utils/validate-config.util.ts @@ -0,0 +1,28 @@ +import { plainToClass } from 'class-transformer'; + +import { validateSync } from 'class-validator'; + +import { ClassConstructor } from 'class-transformer/types/interfaces'; + +/** + * Validates and converts a configuration object to a specified class instance. + * + * @template T - The type of the class to which the configuration will be converted. + * @param {Record} config - The configuration object to validate and convert. + * @param {ClassConstructor} envVariablesClass - The class constructor to which the configuration will be converted. + * @returns {T} - The validated and converted configuration object. + * @throws {Error} - Throws an error if validation fails. + */ +export function validateConfigUtil(config: Record, envVariablesClass: ClassConstructor): T { + const validatedConfig = plainToClass(envVariablesClass, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new Error(errors.toString()); + } + return validatedConfig; +} diff --git a/src/utils/validation-options.util.ts b/src/utils/validation-options.util.ts new file mode 100644 index 0000000..bdaa7ca --- /dev/null +++ b/src/utils/validation-options.util.ts @@ -0,0 +1,74 @@ +import { HttpStatus, UnprocessableEntityException, ValidationError, ValidationPipeOptions } from '@nestjs/common'; + +/** + * Generate errors from validation errors. + * @description This function generates errors from validation errors. + * @param {ValidationError[]} errors The validation errors. + * @returns {unknown} The generated errors. + * @example + * generateErrors([ + * { + * property: 'email', + * constraints: { + * isEmail: 'email must be an email', + * }, + * children: [], + * }, + * { + * property: 'password', + * constraints: { + * isNotEmpty: 'password should not be empty', + * }, + * children: [], + * }, + * ]); + */ +const generateErrors = (errors: ValidationError[]) => { + return errors.map((error) => ({ + property: error.property, + errors: (error.children?.length ?? 0) > 0 ? generateErrors(error.children ?? []) : Object.values(error.constraints ?? []), + })); +}; + +/** + * Validation options util. + * @description This is the validation options util. + */ +/** + * Utility object for configuring validation options in a NestJS application. + * + * @constant + * @type {ValidationPipeOptions} + * + * @property {boolean} transform - Transform the incoming value to the type defined in the DTO. + * @property {boolean} whitelist - Remove any extra properties sent by the client. + * @property {boolean} always - Always transform the incoming value to the type defined in the DTO. + * @property {boolean} forbidNonWhitelisted - Don't throw an error if extra properties are sent by the client. + * @property {boolean} enableDebugMessages - Enable debug messages for the validation pipe. + * @property {object} transformOptions - Options for transforming incoming values. + * @property {boolean} transformOptions.enableImplicitConversion - Enable implicit conversion of incoming values. + * @property {boolean} transformOptions.enableCircularCheck - Enable circular check for incoming values. + * @property {boolean} forbidUnknownValues - Throw an error if unknown values are sent by the client. + * @property {HttpStatus} errorHttpStatusCode - Set the HTTP status code for validation errors. + * @property {Function} exceptionFactory - Factory function to create an exception for validation errors. + * @param {ValidationError[]} errors - Array of validation errors. + * @returns {UnprocessableEntityException} - Exception containing validation errors. + */ +export const validationOptionsUtil: ValidationPipeOptions = { + transform: true, // transform the incoming value to the type defined in the DTO + whitelist: true, // remove any extra properties sent by the client + always: true, // always transform the incoming value to the type defined in the DTO + forbidNonWhitelisted: false, // don't throw an error if extra properties are sent by the client + enableDebugMessages: true, // enable debug messages for the validation pipe + transformOptions: { + enableImplicitConversion: true, // enable implicit conversion of incoming values + enableCircularCheck: true, // enable circular check for incoming values + }, + forbidUnknownValues: false, // throw an error if unknown values are sent by the client + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, // set the HTTP status code for validation errors + exceptionFactory: (errors: ValidationError[]) => { + return new UnprocessableEntityException({ + validationErrors: generateErrors(errors), + }); + }, +};