From f38aac66617325d8698bd72d1697f11d947dbab4 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 03:36:34 -0500 Subject: [PATCH 1/9] chore(project): feat fix and refactor project feat: new environment vars - Implemented new environment vars on .env.example feat: add number and validation options utilities: - Added number.util and validation-options.util files to the utils directory. - The number.util file exports utility functions related to numbers. - The validation-options.util file exports validation options utility for use with NestJS validation pipes. feat: implements swagger/cli plugin: - NestJS swagger plugin implemented fix: inject ConfigService into LoggerModule: - delete app.config.ts file, and implements forRootAsync configuration for logger module. refactor: Refactor on config folder: - Separate all configurations of the app, into independents files into config folder, using the best practices offer for NestJS Docs. --- .gitignore | 9 +- .keys/.gitkeep | 0 README.md | 83 ++++++---- example.env | 74 +++++---- nest-cli.json | 11 +- package-lock.json | 4 + package.json | 3 + src/app.config.ts | 52 ------ src/app.module.ts | 86 +++++++++- src/config/api.config.ts | 14 ++ src/config/app.config.ts | 34 ++++ src/config/auth.config.ts | 18 ++ src/config/database.config.ts | 13 +- src/config/index.ts | 23 --- src/config/infra.config.ts | 12 ++ src/config/jwt.config.ts | 11 -- src/config/swagger.config.ts | 24 +++ src/main.ts | 154 ++++++++++++++---- src/modules/auth/auth.module.ts | 24 +-- src/modules/auth/auth.service.ts | 38 ++--- .../auth/strategies/jwt-user.strategy.ts | 8 +- src/shared/enums/index.ts | 2 - src/shared/enums/log-level.enum.ts | 9 - src/shared/enums/node-env.enum.ts | 5 - src/utils/index.ts | 2 + src/utils/number.util.ts | 42 +++++ src/utils/validation-options.util.ts | 53 ++++++ 27 files changed, 553 insertions(+), 255 deletions(-) create mode 100644 .keys/.gitkeep delete mode 100644 src/app.config.ts create mode 100644 src/config/api.config.ts create mode 100644 src/config/app.config.ts create mode 100644 src/config/auth.config.ts delete mode 100644 src/config/index.ts create mode 100644 src/config/infra.config.ts delete mode 100644 src/config/jwt.config.ts create mode 100644 src/config/swagger.config.ts delete mode 100644 src/shared/enums/log-level.enum.ts delete mode 100644 src/shared/enums/node-env.enum.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/number.util.ts create mode 100644 src/utils/validation-options.util.ts 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/README.md b/README.md index 8ff5574..8bfa701 100644 --- a/README.md +++ b/README.md @@ -2,56 +2,59 @@ 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 + ``` - ```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 + + ``` shell + openssl rsa -in .keys/private_key.pem -outform PEM -pubout -out .keys/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). - Note: The key size (2048 bits in this example) can be adjusted to suit your security requirements. + 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). **DONT 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 + + ```shell + openssl base64 -A -in .keys/public_key.pem -out .keys/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. + + Copy the contents of the `.keys/public_key_base64.txt` file and paste it into `JWT_PUBLIC_KEY` variable declared on your `.env` file. 4. Encode the private key in base64 encoding: + ```sh - openssl base64 -A -in private_key.pem -out private_key_base64.txt + openssl base64 -A -in .keys/private_key.pem -out .keys/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. - -5. Remove the public_key.pem and private_key.pem files. - -6. Remove the public_key_base64.txt and private_key_base64.txt files. + Copy the contents of the `.keys/private_key_base64.txt` file and paste it into `JWT_PUBLJWT_PRIVATE_KEYIC_KEY` variable declared on your `.env` file. ## Running the Application + To run the application in development mode, use the following command: + ```bash npm run start:dev ``` @@ -59,11 +62,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,7 +91,6 @@ 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 ```bash @@ -98,16 +102,20 @@ nestjs-starter-kit/ ├── .husky/ │ ├── commit-msg │ └── pre-commit +├── .keys/ +│ ├── .gitkeep ├── src -│   ├── app.config.ts │   ├── app.controller.spec.ts │   ├── app.controller.ts │   ├── app.module.ts │   ├── app.service.ts │   ├── config +│   │   ├── api.config.ts +│   │   ├── app.config.ts +│   │   ├── auth.config.ts │   │   ├── database.config.ts -│   │   ├── index.ts -│   │   └── jwt.config.ts +│   │   ├── infra.config.ts +│   │   └── swagger.config.ts │   ├── exceptions │   │   ├── bad-request.exception.ts │   │   ├── exceptions.constants.ts @@ -121,7 +129,7 @@ nestjs-starter-kit/ │   │   ├── bad-request-exception.filter.ts │   │   ├── forbidden-exception.filter.ts │   │   ├── index.ts -│   │   ├── internal-server-error-exception.filter.ts +│   │   ├── internal-server-error-exception.filter.tsFilter │   │   ├── not-found-exception.filter.ts │   │   ├── unauthorized-exception.filter.ts │   │   └── validator-exception.filter.ts @@ -163,11 +171,13 @@ nestjs-starter-kit/ │   ├── enums │   │   ├── db.enum.ts │   │   ├── index.ts -│   │   ├── log-level.enum.ts -│   │   └── node-env.enum.ts │   └── types │   ├── index.ts │   └── schema.type.ts +│   └── utils +│   ├── index.ts +│   ├── number.util.ts +│   └── validation-options.util.ts ├── test │   ├── app.e2e-spec.ts │   └── jest-e2e.json @@ -190,6 +200,7 @@ nestjs-starter-kit/ ├── package.json ├── renovate.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,7 +212,6 @@ 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. @@ -209,7 +219,7 @@ This project follows a structured organization to maintain clean, scalable code, #### Subdirectories within `src/` -- **`config/`**: Stores configuration files (e.g., `database.config.ts`, `jwt.config.ts`) for different aspects of the application. +- **`config/`**: Stores configuration files (e.g., `database.config.ts`, `auth.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. - **`modules/`**: Contains feature modules of the application: @@ -217,8 +227,9 @@ This project follows a structured organization to maintain clean, scalable code, - **`user/`**: Manages user-related operations, including controllers, services, repositories, and schemas. - **`workspace/`**: Manages workspace-related functionality, with services, repositories, and schemas. - **`shared/`**: Contains shared resources and utilities: - - **`enums/`**: Defines enumerations (e.g., `db.enum.ts`, `node-env.enum.ts`) used across the application. + - **`enums/`**: Defines enumerations (e.g., `db.enum.ts`) used across the application. - **`types/`**: Custom TypeScript types (e.g., `schema.type.ts`) used for type safety throughout the codebase. +- **`utils/`**: Utility functions (e.g., `number.util.ts`, `validation-options.util.ts`) for common operations. ### 3. Testing (`test/`) diff --git a/example.env b/example.env index 12698d4..c94b10d 100644 --- a/example.env +++ b/example.env @@ -1,32 +1,42 @@ -## 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" + +# ========================================================================================= +# INFRA CONFIGURATION +# ========================================================================================= +CLUSTERING = false # Whether to enable clustering mode for the application (true or false) + +# ========================================================================================= +# DATABASE CONFIGURATION +# ========================================================================================= +MONGODB_URI = "mongodb://localhost:27017/nestjs_starter_kit" # The URI for the MongoDB database used by the application + +# ========================================================================================= +# 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" # Swagger docs title +SWAGGER_DESCRIPTION = "The API for the NestJS Starter project" # Swagger description +SWAGGER_VERSION = "1.0" # Swagger version +SWAGGER_PATH = "docs" # Swagger docs url path +SWAGGER_JSON_PATH = "docs/json" # Swagger JSON url path +SWAGGER_YAML_PATH = "docs/yml" # Swagger YAML url path diff --git a/nest-cli.json b/nest-cli.json index f9aa683..fe4f69b 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,15 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "classValidatorShim": false, + "introspectComments": true + } + } + ] } } diff --git a/package-lock.json b/package-lock.json index db8cd6d..1a90700 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,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": { @@ -6485,6 +6488,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" }, diff --git a/package.json b/package.json index 66b7da0..b24cacb 100644 --- a/package.json +++ b/package.json @@ -83,5 +83,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.module.ts b/src/app.module.ts index 13cdb3b..dc2c541 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,18 +1,18 @@ // 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'; -// Import application files +import * as crypto from 'crypto'; + +import { LoggerModule } from 'nestjs-pino'; + +import { IncomingMessage } from 'http'; +import { ServerResponse } from 'http'; -import { AppConfig } from './app.config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { configuration } from './config/index'; - -// Import filters import { AllExceptionsFilter, BadRequestExceptionFilter, @@ -21,6 +21,13 @@ import { UnauthorizedExceptionFilter, ValidationExceptionFilter, } from './filters'; + +import apiConfig from './config/api.config'; +import appConfig, { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from './config/app.config'; +import authConfig from './config/auth.config'; +import databaseConfig from './config/database.config'; +import infraConfig from './config/infra.config'; +import swaggerConfig from './config/swagger.config'; import { AuthModule } from './modules/auth/auth.module'; import { UserModule } from './modules/user/user.module'; import { WorkspaceModule } from './modules/workspace/workspace.module'; @@ -32,11 +39,70 @@ import { WorkspaceModule } from './modules/workspace/workspace.module'; // Configure environment variables ConfigModule.forRoot({ isGlobal: true, // Make the configuration global - load: [configuration], // Load the environment variables from the configuration file + 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 + }, }), // Configure logging - LoggerModule.forRoot(AppConfig.getLoggerConfig()), // ! forRootAsync is not working with ConfigService in nestjs-pino + LoggerModule.forRootAsync({ + inject: [ConfigService], // Inject the ConfigService into the factory function + // useFactory: async (configService: ConfigService) => AppConfig.getLoggerConfig(), + 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, + }, + }; + }, + }), // Configure mongoose MongooseModule.forRootAsync({ @@ -44,7 +110,9 @@ import { WorkspaceModule } from './modules/workspace/workspace.module'; 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'), + uri: configService.get('database.uri', { + infer: true, + }), }), }), // Import other modules diff --git a/src/config/api.config.ts b/src/config/api.config.ts new file mode 100644 index 0000000..e25686b --- /dev/null +++ b/src/config/api.config.ts @@ -0,0 +1,14 @@ +import { registerAs } from '@nestjs/config'; + +export interface ApiConfig { + prefixEnabled: boolean; + prefix: string; +} + +export default registerAs( + 'api', + (): ApiConfig => ({ + prefixEnabled: process.env.API_PREFIX_ENABLED && process.env.API_PREFIX_ENABLED === 'false' ? false : true, + 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..ef185c4 --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,34 @@ +import { registerAs } from '@nestjs/config'; + +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 interface AppConfig { + env: string; + port: number; + host: string; + logLevel: string; +} + +export default registerAs( + 'app', + (): AppConfig => ({ + 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..ad4c932 --- /dev/null +++ b/src/config/auth.config.ts @@ -0,0 +1,18 @@ +import { registerAs } from '@nestjs/config'; + +export interface AuthenticationConfig { + bcryptSaltRounds: number; + jwtPrivateKey: string; + jwtPublicKey: string; + jwtExpiresIn: string; +} + +export default registerAs( + 'authentication', + (): AuthenticationConfig => ({ + bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS, 10) || 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..9798a9a 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,7 +1,12 @@ -export interface IDatabaseConfig { +import { registerAs } from '@nestjs/config'; + +export interface DatabaseConfig { uri: string; } -export const databaseConfig = (): IDatabaseConfig => ({ - uri: process.env.MONGODB_URI, -}); +export default registerAs( + 'database', + (): DatabaseConfig => ({ + uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/nestjs', + }), +); 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..e59a93f --- /dev/null +++ b/src/config/infra.config.ts @@ -0,0 +1,12 @@ +import { registerAs } from '@nestjs/config'; + +export interface InfraConfig { + clusteringEnabled: boolean; +} + +export default registerAs( + 'infra', + (): InfraConfig => ({ + clusteringEnabled: process.env.CLUSTERING && process.env.CLUSTERING === 'true' ? true : false, + }), +); 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..d9c6510 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,24 @@ +import { registerAs } from '@nestjs/config'; + +export interface SwaggerConfig { + swaggerEnabled: boolean; + swaggerTitle: string; + swaggerDescription: string; + swaggerVersion: string; + swaggerPath: string; + swaggerJsonPath: string; + swaggerYamlPath: string; +} + +export default registerAs( + 'swagger', + (): SwaggerConfig => ({ + 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/main.ts b/src/main.ts index 736ecbf..c1f5b39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,63 +1,151 @@ // 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 { NestFactory } from '@nestjs/core'; +import { ClassSerializerInterceptor, Logger, ValidationPipe } from '@nestjs/common'; +import { NestFactory, Reflector } 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'; // 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 main function -async function bootstrap() { +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, }); // Use the Pino logger for the application app.useLogger(app.get(Pino)); - // Allow all origins - app.enableCors(); + // Use the application container + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + + // Get configuration service from the application + const configService: ConfigService = app.get(ConfigService); - // 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', - }, + // 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(); + } - // Get the configuration service from the application - const configService = app.get(ConfigService); + // 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 port number from the configuration - const PORT = configService.get('port'); + // Set up the validation pipe + app.useGlobalPipes(new ValidationPipe(validationOptionsUtil)); + + // Set the global interceptors + app.useGlobalInterceptors( + // Use the class serializer interceptor to serialize the response objects + new ClassSerializerInterceptor(app.get(Reflector)), + ); + + // Allow all origins + app.enableCors(); + + // 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}`); + } + }); - // 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/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 50f44a5..ecd7df1 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,28 +1,30 @@ // 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/jwt-user.strategy'; @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,7 +33,7 @@ import { WorkspaceModule } from '../workspace/workspace.module'; UserModule, WorkspaceModule, ], - providers: [JwtUserStrategy, AuthService], + providers: [ConfigService, JwtUserStrategy, AuthService], controllers: [AuthController], exports: [JwtUserStrategy, PassportModule], }) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index c404fca..3b7abfd 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,29 +1,29 @@ // 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 } from 'src/utils'; -// 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'; - -// 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/jwt-user-payload.interface'; + @Injectable() export class AuthService { - private readonly SALT_ROUNDS = 10; - constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, private readonly userQueryService: UserQueryService, private readonly workspaceQueryService: WorkspaceQueryService, - private readonly jwtService: JwtService, ) {} async signup(signupReqDto: SignupReqDto): Promise { @@ -40,7 +40,9 @@ export class AuthService { const workspace = await this.workspaceQueryService.create(workspacePayload); // Hash password - const saltOrRounds = this.SALT_ROUNDS; + const saltOrRounds = this.configService.get('authentication.bcryptSaltRounds', { + infer: true, + }); const hashedPassword = await bcrypt.hash(password, saltOrRounds); const userPayload: User = { @@ -49,7 +51,7 @@ export class AuthService { workspace: workspace._id, name, verified: true, - registerCode: this.generateCode(), + registerCode: generateOTPCode(), verificationCode: null, verificationCodeExpiry: null, resetToken: null, @@ -62,16 +64,6 @@ 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; diff --git a/src/modules/auth/strategies/jwt-user.strategy.ts b/src/modules/auth/strategies/jwt-user.strategy.ts index d40c9c7..c2928c0 100644 --- a/src/modules/auth/strategies/jwt-user.strategy.ts +++ b/src/modules/auth/strategies/jwt-user.strategy.ts @@ -1,11 +1,12 @@ import { ConfigService } from '@nestjs/config'; -import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + import { JwtUserPayload } from '../interfaces/jwt-user-payload.interface'; -import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; import { UserQueryService } from '../../user/user.query.service'; +import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; @Injectable() export class JwtUserStrategy extends PassportStrategy(Strategy, 'authUser') { @@ -15,7 +16,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/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/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..bb31453 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './number.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..3d08f40 --- /dev/null +++ b/src/utils/number.util.ts @@ -0,0 +1,42 @@ +/** + * Generates a random six digit OTP + * @param {number} min - the minimum value of the OTP (default is 100000) + * @param {number} max - the maximum value of the OTP (default is 999999) + * @returns {number} - returns the generated OTP + * @throws {Error} - throws an error if the minimum value is greater than the maximum value + * @throws {Error} - throws an error if the minimum or maximum values are negative + * @throws {Error} - throws an error if the minimum or maximum values are not six digits long + * @throws {Error} - throws an error if the minimum and maximum values are the same + * @throws {Error} - throws an error if the minimum or maximum values are greater than 999999 + * @throws {Error} - throws an error if the minimum or maximum values are less than 100000 + * @throws {Error} - throws an error if the minimum or maximum values are not numbers + */ +export const generateOTPCode = (min?: number, max?: number): number => { + min = min || 100000; + max = max || 999999; + + // Validate the input values + 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/validation-options.util.ts b/src/utils/validation-options.util.ts new file mode 100644 index 0000000..416fa08 --- /dev/null +++ b/src/utils/validation-options.util.ts @@ -0,0 +1,53 @@ +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[]): unknown => { + return errors.reduce( + (accumulator, currentValue) => ({ + ...accumulator, + [currentValue.property]: + (currentValue.children?.length ?? 0) > 0 + ? generateErrors(currentValue.children ?? []) + : Object.values(currentValue.constraints ?? {}).join(', '), + }), + {}, + ); +}; + +/** + * Validation options util. + * @description This is the validation options util. + */ +export const validationOptionsUtil: ValidationPipeOptions = { + transform: true, + whitelist: true, + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + exceptionFactory: (errors: ValidationError[]) => { + return new UnprocessableEntityException({ + status: HttpStatus.UNPROCESSABLE_ENTITY, + errors: generateErrors(errors), + }); + }, +}; From 3a3ce9b88ff1fdd11c9ba857896549b9251ad182 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 05:31:04 -0500 Subject: [PATCH 2/9] feat: code refactor and environment variables validation feat: environment vars validation: - Implemented env vars validation using class-validator feat: database module using class and auto populate feature - Created a database module for mongoose - Implemented auto-populate option feat: create log module for logger - implemented log module to sepparate the logger configuration refactor: base code refactor - base code refactor creating the core module --- example.env | 17 +-- package-lock.json | 10 ++ package.json | 1 + src/app.controller.spec.ts | 23 ---- src/app.controller.ts | 15 --- src/app.module.ts | 105 +----------------- src/config/api.config.ts | 30 +++-- src/config/app.config.ts | 36 +++++- src/config/auth.config.ts | 38 +++++-- src/config/database.config.ts | 45 ++++++-- src/config/infra.config.ts | 22 +++- src/config/swagger.config.ts | 46 +++++++- .../application/application.controller.ts | 15 +++ src/core/application/application.module.ts | 41 +++++++ .../application/application.service.ts} | 2 +- src/core/core.module.ts | 11 ++ src/core/database/database.module.ts | 16 +++ src/core/database/database.service.ts | 34 ++++++ src/core/log/log.module.ts | 69 ++++++++++++ src/utils/index.ts | 1 + src/utils/validate-config.util.ts | 19 ++++ 21 files changed, 409 insertions(+), 187 deletions(-) delete mode 100644 src/app.controller.spec.ts delete mode 100644 src/app.controller.ts create mode 100644 src/core/application/application.controller.ts create mode 100644 src/core/application/application.module.ts rename src/{app.service.ts => core/application/application.service.ts} (78%) create mode 100644 src/core/core.module.ts create mode 100644 src/core/database/database.module.ts create mode 100644 src/core/database/database.service.ts create mode 100644 src/core/log/log.module.ts create mode 100644 src/utils/validate-config.util.ts diff --git a/example.env b/example.env index c94b10d..56070ef 100644 --- a/example.env +++ b/example.env @@ -10,7 +10,7 @@ LOG_LEVEL = "info" # The logging level for the application (e.g. debug, in # API CONFIGURATION # ========================================================================================= API_PREFIX_ENABLED = true # Enable or disable API prefix -API_PREFIX = "api" +API_PREFIX = "api" # The API prefix # ========================================================================================= # INFRA CONFIGURATION @@ -21,6 +21,9 @@ CLUSTERING = false # Whether to enable clustering mode for the application (true # 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 @@ -34,9 +37,9 @@ JWT_EXPIRATION_TIME = "" # The expiration time for JSON Web Tokens (JWTs) expres # SWAGGER CONFIGURATION # ========================================================================================= SWAGGER_ENABLED = true # Enable swagger documentation -SWAGGER_TITLE = "NestJS Starter API" # Swagger docs title -SWAGGER_DESCRIPTION = "The API for the NestJS Starter project" # Swagger description -SWAGGER_VERSION = "1.0" # Swagger version -SWAGGER_PATH = "docs" # Swagger docs url path -SWAGGER_JSON_PATH = "docs/json" # Swagger JSON url path -SWAGGER_YAML_PATH = "docs/yml" # Swagger YAML url path +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/package-lock.json b/package-lock.json index 1a90700..d24edaf 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", @@ -8951,6 +8952,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", diff --git a/package.json b/package.json index b24cacb..11bd720 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", 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 dc2c541..f32d9d9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,18 +1,6 @@ -// Import required modules -import { APP_FILTER, APP_PIPE } from '@nestjs/core'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { Module, ValidationError, ValidationPipe } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; - -import * as crypto from 'crypto'; - -import { LoggerModule } from 'nestjs-pino'; - -import { IncomingMessage } from 'http'; -import { ServerResponse } from 'http'; +import { APP_FILTER, APP_PIPE } from '@nestjs/core'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { AllExceptionsFilter, BadRequestExceptionFilter, @@ -22,107 +10,20 @@ import { ValidationExceptionFilter, } from './filters'; -import apiConfig from './config/api.config'; -import appConfig, { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from './config/app.config'; -import authConfig from './config/auth.config'; -import databaseConfig from './config/database.config'; -import infraConfig from './config/infra.config'; -import swaggerConfig from './config/swagger.config'; +import { CoreModule } from './core/core.module'; import { AuthModule } from './modules/auth/auth.module'; import { UserModule } from './modules/user/user.module'; import { WorkspaceModule } from './modules/workspace/workspace.module'; -// Import other modules - @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 - }, - }), - - // Configure logging - LoggerModule.forRootAsync({ - inject: [ConfigService], // Inject the ConfigService into the factory function - // useFactory: async (configService: ConfigService) => AppConfig.getLoggerConfig(), - 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, - }, - }; - }, - }), - - // 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', { - infer: true, - }), - }), - }), + CoreModule, // Import other modules AuthModule, UserModule, WorkspaceModule, ], - controllers: [AppController], // Define the application's controller providers: [ - AppService, { provide: APP_FILTER, useClass: AllExceptionsFilter }, { provide: APP_FILTER, useClass: ValidationExceptionFilter }, { provide: APP_FILTER, useClass: BadRequestExceptionFilter }, diff --git a/src/config/api.config.ts b/src/config/api.config.ts index e25686b..51b4eea 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -1,14 +1,30 @@ import { registerAs } from '@nestjs/config'; -export interface ApiConfig { +import { IsBoolean, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +import { validateConfigUtil } from '../utils'; + +export type ApiConfig = { prefixEnabled: boolean; prefix: string; +}; + +class EnvironmentVariablesValidator { + @IsNotEmpty() + @IsBoolean() + API_PREFIX_ENABLED: boolean; + + @IsNotEmpty() + @IsString() + @MinLength(1) + API_PREFIX: string; } -export default registerAs( - 'api', - (): ApiConfig => ({ - prefixEnabled: process.env.API_PREFIX_ENABLED && process.env.API_PREFIX_ENABLED === 'false' ? false : true, +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 index ef185c4..b0ef632 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -1,5 +1,9 @@ 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', @@ -16,19 +20,39 @@ export enum E_APP_LOG_LEVELS { FATAL = 'fatal', } -export interface AppConfig { +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; } -export default registerAs( - 'app', - (): AppConfig => ({ +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 index ad4c932..7ae8cc5 100644 --- a/src/config/auth.config.ts +++ b/src/config/auth.config.ts @@ -1,18 +1,42 @@ import { registerAs } from '@nestjs/config'; -export interface AuthenticationConfig { +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; } -export default registerAs( - 'authentication', - (): AuthenticationConfig => ({ - bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS, 10) || 10, +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 9798a9a..af6c756 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,12 +1,43 @@ import { registerAs } from '@nestjs/config'; -export interface DatabaseConfig { +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 default registerAs( - 'database', - (): DatabaseConfig => ({ - uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/nestjs', - }), -); +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/infra.config.ts b/src/config/infra.config.ts index e59a93f..9723f6e 100644 --- a/src/config/infra.config.ts +++ b/src/config/infra.config.ts @@ -1,12 +1,22 @@ import { registerAs } from '@nestjs/config'; -export interface InfraConfig { +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type InfraConfig = { clusteringEnabled: boolean; +}; + +class EnvironmentVariablesValidator { + @IsBoolean() + @IsNotEmpty() + CLUSTERING: boolean; } -export default registerAs( - 'infra', - (): InfraConfig => ({ +export default registerAs('infra', (): InfraConfig => { + validateConfigUtil(process.env, EnvironmentVariablesValidator); + return { clusteringEnabled: process.env.CLUSTERING && process.env.CLUSTERING === 'true' ? true : false, - }), -); + }; +}); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index d9c6510..38ffa0b 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -1,6 +1,10 @@ import { registerAs } from '@nestjs/config'; -export interface SwaggerConfig { +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +import { validateConfigUtil } from 'src/utils'; + +export type SwaggerConfig = { swaggerEnabled: boolean; swaggerTitle: string; swaggerDescription: string; @@ -8,11 +12,41 @@ export interface SwaggerConfig { 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; } -export default registerAs( - 'swagger', - (): SwaggerConfig => ({ +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', @@ -20,5 +54,5 @@ export default registerAs( 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..2c700cf --- /dev/null +++ b/src/core/application/application.controller.ts @@ -0,0 +1,15 @@ +import { ApiTags } from '@nestjs/swagger'; +import { Controller, Get } from '@nestjs/common'; + +import { ApplicationService } from './application.service'; + +@ApiTags('Health-check') +@Controller() +export class ApplicationController { + constructor(private readonly applicationService: ApplicationService) {} + + @Get() + 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..73f01a0 --- /dev/null +++ b/src/core/application/application.module.ts @@ -0,0 +1,41 @@ +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'; + +@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/app.service.ts b/src/core/application/application.service.ts similarity index 78% rename from src/app.service.ts rename to src/core/application/application.service.ts index 48a63e3..c3b8f97 100644 --- a/src/app.service.ts +++ b/src/core/application/application.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class AppService { +export class ApplicationService { 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..2598072 --- /dev/null +++ b/src/core/core.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationModule } from './application/application.module'; + +@Module({ + imports: [ApplicationModule], + controllers: [], + providers: [], + exports: [], +}) +export class CoreModule {} diff --git a/src/core/database/database.module.ts b/src/core/database/database.module.ts new file mode 100644 index 0000000..e4f55e4 --- /dev/null +++ b/src/core/database/database.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { ConfigService } from '@nestjs/config'; + +import { DatabaseService } from './database.service'; + +@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..85dcd1a --- /dev/null +++ b/src/core/database/database.service.ts @@ -0,0 +1,34 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MongooseModuleOptions, MongooseOptionsFactory } from '@nestjs/mongoose'; + +@Injectable() +export class DatabaseService implements MongooseOptionsFactory { + private readonly logger = new Logger(DatabaseService.name); + + constructor(private readonly configService: ConfigService) {} + + createMongooseOptions(): MongooseModuleOptions { + this.logger.debug('Creating Mongoose options...'); + 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, + }), + connectionFactory(connection) { + if (autoPopulate) { + connection.plugin(require('mongoose-autopopulate')); + } + return connection; + }, + }; + } +} diff --git a/src/core/log/log.module.ts b/src/core/log/log.module.ts new file mode 100644 index 0000000..9dfd4d9 --- /dev/null +++ b/src/core/log/log.module.ts @@ -0,0 +1,69 @@ +import { Module } 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'; + +@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, + }, + }; + }, + }), + ], + controllers: [], + providers: [], + exports: [], +}) +export class LogModule {} diff --git a/src/utils/index.ts b/src/utils/index.ts index bb31453..58740ba 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './number.util'; +export * from './validate-config.util'; export * from './validation-options.util'; diff --git a/src/utils/validate-config.util.ts b/src/utils/validate-config.util.ts new file mode 100644 index 0000000..187e169 --- /dev/null +++ b/src/utils/validate-config.util.ts @@ -0,0 +1,19 @@ +import { plainToClass } from 'class-transformer'; + +import { validateSync } from 'class-validator'; + +import { ClassConstructor } from 'class-transformer/types/interfaces'; + +export function validateConfigUtil(config: Record, envVariablesClass: ClassConstructor) { + const validatedConfig = plainToClass(envVariablesClass, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new Error(errors.toString()); + } + return validatedConfig; +} From fc89ab277adbd7407327ab7267b9ab744d879f58 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 16:24:50 -0500 Subject: [PATCH 3/9] feat(core): enhance application structure and error handling - Refactored `app.module.ts` to improve module imports and providers organization - Added `CoreModule` to centralize core functionalities - Moved exception filters to `core/filters` for better modularity - Introduced `TimeoutInterceptor` in `core/interceptors` to handle request timeouts - Added `JwtUserAuthGuard` to `AuthModule` for enhanced security - Updated providers to include new filters and interceptors: - `AllExceptionsFilter` for global exception handling - `ValidationExceptionFilter` for validation errors - `BadRequestExceptionFilter` for bad request errors - `UnauthorizedExceptionFilter` for unauthorized access - `ForbiddenExceptionFilter` for forbidden access - `NotFoundExceptionFilter` for handling 404 errors - `GatewayTimeOutExceptionFilter` for handling gateway timeouts - Improved validation pipe configuration to use `exceptionFactory` for custom error handling This commit enhances the overall structure and error handling mechanisms of the application, making it more modular and maintainable. --- src/app.module.ts | 72 +++++++---- src/config/api.config.ts | 2 +- .../application/application.controller.ts | 5 +- .../abstracts/database.abstract.interface.ts | 5 + .../abstracts/database.abstract.repository.ts | 39 ++++++ src/core/database/abstracts/index.ts | 2 + src/core/database/database-schema-options.ts | 40 ++++++ src/core/database/database.service.ts | 10 +- src/core/exceptions/all.exception.ts | 114 ++++++++++++++++++ .../exceptions/bad-request.exception.ts | 5 +- .../constants}/exceptions.constants.ts | 0 src/core/exceptions/constants/index.ts | 1 + .../exceptions/forbidden.exception.ts | 5 +- .../exceptions/gateway-timeout.exception.ts | 80 ++++++++++++ src/{ => core}/exceptions/index.ts | 6 +- .../interfaces}/exceptions.interface.ts | 18 +++ src/core/exceptions/interfaces/index.ts | 1 + .../internal-server-error.exception.ts | 5 +- .../exceptions/unauthorized.exception.ts | 5 +- .../filters/all-exception.filter.ts | 0 .../filters/bad-request-exception.filter.ts | 4 +- .../filters/forbidden-exception.filter.ts | 4 +- .../gateway-timeout.exception.filter.ts | 28 +++++ src/{ => core}/filters/index.ts | 6 +- .../internal-server-error-exception.filter.ts | 4 +- .../filters/not-found-exception.filter.ts | 17 +-- .../filters/unauthorized-exception.filter.ts | 4 +- .../filters/validator-exception.filter.ts | 1 + src/core/interceptors/index.ts | 2 + .../interceptors/serializer.interceptor.ts | 13 ++ src/core/interceptors/timeout.interceptor.ts | 28 +++++ src/core/log/log.module.ts | 12 +- src/core/middlewares/index.ts | 1 + src/core/middlewares/logging.middleware.ts | 22 ++++ src/main.ts | 22 ++-- src/modules/auth/auth.controller.ts | 15 +-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/auth.service.ts | 8 +- src/modules/auth/decorators/index.ts | 2 + .../auth/decorators/public.decorator.ts | 4 + src/modules/auth/guards/index.ts | 1 + .../auth/guards/jwt-user-auth.guard.ts | 14 ++- src/modules/auth/interfaces/index.ts | 1 + src/modules/auth/strategies/index.ts | 1 + .../auth/strategies/jwt-user.strategy.ts | 5 +- src/modules/user/user.controller.ts | 8 +- src/modules/user/user.module.ts | 3 +- src/modules/user/user.query.service.ts | 12 +- src/modules/user/user.repository.ts | 22 ++-- src/modules/user/user.schema.ts | 60 ++++----- src/modules/workspace/workspace.module.ts | 3 +- .../workspace/workspace.query-service.ts | 4 +- src/modules/workspace/workspace.repository.ts | 33 ++--- src/modules/workspace/workspace.schema.ts | 49 +++----- src/shared/index.ts | 2 + src/shared/types/index.ts | 1 + src/shared/types/or-never.type.ts | 1 + src/utils/date-time.util.ts | 17 +++ src/utils/deep-resolver.util.ts | 28 +++++ src/utils/document-entity-helper.util.ts | 42 +++++++ src/utils/index.ts | 3 + src/utils/validation-options.util.ts | 33 ++--- 62 files changed, 740 insertions(+), 218 deletions(-) create mode 100644 src/core/database/abstracts/database.abstract.interface.ts create mode 100644 src/core/database/abstracts/database.abstract.repository.ts create mode 100644 src/core/database/abstracts/index.ts create mode 100644 src/core/database/database-schema-options.ts create mode 100644 src/core/exceptions/all.exception.ts rename src/{ => core}/exceptions/bad-request.exception.ts (97%) rename src/{exceptions => core/exceptions/constants}/exceptions.constants.ts (100%) create mode 100644 src/core/exceptions/constants/index.ts rename src/{ => core}/exceptions/forbidden.exception.ts (95%) create mode 100644 src/core/exceptions/gateway-timeout.exception.ts rename src/{ => core}/exceptions/index.ts (55%) rename src/{exceptions => core/exceptions/interfaces}/exceptions.interface.ts (68%) create mode 100644 src/core/exceptions/interfaces/index.ts rename src/{ => core}/exceptions/internal-server-error.exception.ts (97%) rename src/{ => core}/exceptions/unauthorized.exception.ts (98%) rename src/{ => core}/filters/all-exception.filter.ts (100%) rename src/{ => core}/filters/bad-request-exception.filter.ts (90%) rename src/{ => core}/filters/forbidden-exception.filter.ts (88%) create mode 100644 src/core/filters/gateway-timeout.exception.filter.ts rename src/{ => core}/filters/index.ts (71%) rename src/{ => core}/filters/internal-server-error-exception.filter.ts (90%) rename src/{ => core}/filters/not-found-exception.filter.ts (65%) rename src/{ => core}/filters/unauthorized-exception.filter.ts (88%) rename src/{ => core}/filters/validator-exception.filter.ts (99%) create mode 100644 src/core/interceptors/index.ts create mode 100644 src/core/interceptors/serializer.interceptor.ts create mode 100644 src/core/interceptors/timeout.interceptor.ts create mode 100644 src/core/middlewares/index.ts create mode 100644 src/core/middlewares/logging.middleware.ts create mode 100644 src/modules/auth/decorators/index.ts create mode 100644 src/modules/auth/decorators/public.decorator.ts create mode 100644 src/modules/auth/guards/index.ts create mode 100644 src/modules/auth/interfaces/index.ts create mode 100644 src/modules/auth/strategies/index.ts create mode 100644 src/shared/index.ts create mode 100644 src/shared/types/or-never.type.ts create mode 100644 src/utils/date-time.util.ts create mode 100644 src/utils/deep-resolver.util.ts create mode 100644 src/utils/document-entity-helper.util.ts diff --git a/src/app.module.ts b/src/app.module.ts index f32d9d9..079a99d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,17 +1,19 @@ -import { Module, ValidationError, ValidationPipe } from '@nestjs/common'; -import { APP_FILTER, APP_PIPE } from '@nestjs/core'; +import { Module } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { CoreModule } from './core/core.module'; import { AllExceptionsFilter, BadRequestExceptionFilter, - ForbiddenExceptionFilter, NotFoundExceptionFilter, UnauthorizedExceptionFilter, + ForbiddenExceptionFilter, + GatewayTimeOutExceptionFilter, ValidationExceptionFilter, -} from './filters'; - -import { CoreModule } from './core/core.module'; +} from './core/filters'; +import { TimeoutInterceptor } from './core/interceptors'; import { AuthModule } from './modules/auth/auth.module'; +import { JwtUserAuthGuard } from './modules/auth/guards'; import { UserModule } from './modules/user/user.module'; import { WorkspaceModule } from './modules/workspace/workspace.module'; @@ -24,24 +26,46 @@ import { WorkspaceModule } from './modules/workspace/workspace.module'; WorkspaceModule, ], providers: [ - { 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]; - }, - }), - }, - ], // Define the application's service + useClass: ValidationExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: BadRequestExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: UnauthorizedExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: ForbiddenExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: NotFoundExceptionFilter, + }, + { + provide: APP_FILTER, + useClass: GatewayTimeOutExceptionFilter, + }, + { + provide: APP_INTERCEPTOR, + useFactory: () => { + // TODO: Move this to config + const timeoutInMilliseconds = 30000; + return new TimeoutInterceptor(timeoutInMilliseconds); + }, + }, + { + provide: APP_GUARD, + useClass: JwtUserAuthGuard, + }, + ], }) export class AppModule {} diff --git a/src/config/api.config.ts b/src/config/api.config.ts index 51b4eea..1433b1f 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -2,7 +2,7 @@ import { registerAs } from '@nestjs/config'; import { IsBoolean, IsNotEmpty, IsString, MinLength } from 'class-validator'; -import { validateConfigUtil } from '../utils'; +import { validateConfigUtil } from 'src/utils'; export type ApiConfig = { prefixEnabled: boolean; diff --git a/src/core/application/application.controller.ts b/src/core/application/application.controller.ts index 2c700cf..cd1bfeb 100644 --- a/src/core/application/application.controller.ts +++ b/src/core/application/application.controller.ts @@ -1,14 +1,17 @@ import { ApiTags } from '@nestjs/swagger'; import { Controller, Get } from '@nestjs/common'; +import { Public } from 'src/modules/auth/decorators'; + import { ApplicationService } from './application.service'; -@ApiTags('Health-check') +@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/database/abstracts/database.abstract.interface.ts b/src/core/database/abstracts/database.abstract.interface.ts new file mode 100644 index 0000000..d0998e8 --- /dev/null +++ b/src/core/database/abstracts/database.abstract.interface.ts @@ -0,0 +1,5 @@ +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..f56737f --- /dev/null +++ b/src/core/database/abstracts/database.abstract.repository.ts @@ -0,0 +1,39 @@ +import { Logger, NotFoundException } from '@nestjs/common'; + +import { Model } from 'mongoose'; + +export abstract class DatabaseAbstractRepository { + private readonly log: Logger; + + constructor(private readonly model: Model) { + this.log = new Logger(this.constructor.name); + } + + async findAllDocuments(): Promise { + return this.model.find().exec(); + } + + 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; + } + + 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; + } + + 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..20080ef --- /dev/null +++ b/src/core/database/database-schema-options.ts @@ -0,0 +1,40 @@ +import { SchemaOptions } from '@nestjs/mongoose'; + +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.service.ts b/src/core/database/database.service.ts index 85dcd1a..d306e47 100644 --- a/src/core/database/database.service.ts +++ b/src/core/database/database.service.ts @@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MongooseModuleOptions, MongooseOptionsFactory } from '@nestjs/mongoose'; +import { Connection } from 'mongoose'; + @Injectable() export class DatabaseService implements MongooseOptionsFactory { private readonly logger = new Logger(DatabaseService.name); @@ -9,10 +11,11 @@ export class DatabaseService implements MongooseOptionsFactory { constructor(private readonly configService: ConfigService) {} createMongooseOptions(): MongooseModuleOptions { - this.logger.debug('Creating Mongoose options...'); + this.logger.error('Creating Mongoose options...'); const autoPopulate: boolean = this.configService.get('database.autoPopulate', { infer: true, }); + this.logger.error(`Auto-populate: ${autoPopulate}`); return { uri: this.configService.get('database.uri', { infer: true, @@ -23,11 +26,10 @@ export class DatabaseService implements MongooseOptionsFactory { heartbeatFrequencyMS: this.configService.get('database.heartbeatFrequencyMS', { infer: true, }), - connectionFactory(connection) { + onConnectionCreate(connection: Connection) { if (autoPopulate) { - connection.plugin(require('mongoose-autopopulate')); + connection.plugin(require('mongoose-autopopulate')); // Enable the mongoose-autopopulate plugin } - return connection; }, }; } diff --git a/src/core/exceptions/all.exception.ts b/src/core/exceptions/all.exception.ts new file mode 100644 index 0000000..6233092 --- /dev/null +++ b/src/core/exceptions/all.exception.ts @@ -0,0 +1,114 @@ +/** + * 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'; + +export class AllException extends HttpException { + @ApiProperty({ + enum: ExceptionConstants.InternalServerErrorCodes, + description: 'A unique code identifying the error.', + example: ExceptionConstants.InternalServerErrorCodes.UNEXPECTED_ERROR, + }) + code: number; // Internal status code + + @ApiProperty({ + description: 'Message for the exception', + example: 'Internal Server Error', + }) + message: string; // Message for the exception + + @ApiProperty({ + description: 'A description of the error message.', + example: 'An unexpected error occurred', + }) + description: string; // Description of the exception + + @ApiPropertyOptional({ + description: 'The cause of the exception', + example: {}, + }) + error?: unknown; // Error object + + @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 + + /** + * 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 97% rename from src/exceptions/bad-request.exception.ts rename to src/core/exceptions/bad-request.exception.ts index a505a27..e385e7b 100644 --- a/src/exceptions/bad-request.exception.ts +++ b/src/core/exceptions/bad-request.exception.ts @@ -6,9 +6,8 @@ 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'; export class BadRequestException extends HttpException { @ApiProperty({ diff --git a/src/exceptions/exceptions.constants.ts b/src/core/exceptions/constants/exceptions.constants.ts similarity index 100% rename from src/exceptions/exceptions.constants.ts rename to src/core/exceptions/constants/exceptions.constants.ts 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 95% rename from src/exceptions/forbidden.exception.ts rename to src/core/exceptions/forbidden.exception.ts index daad724..c6d64c4 100644 --- a/src/exceptions/forbidden.exception.ts +++ b/src/core/exceptions/forbidden.exception.ts @@ -6,9 +6,8 @@ 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. diff --git a/src/core/exceptions/gateway-timeout.exception.ts b/src/core/exceptions/gateway-timeout.exception.ts new file mode 100644 index 0000000..506ccd5 --- /dev/null +++ b/src/core/exceptions/gateway-timeout.exception.ts @@ -0,0 +1,80 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { HttpException, HttpStatus } from '@nestjs/common'; + +import { ExceptionConstants } from './constants'; +import { IException, IHttpGatewayTimeOutExceptionResponse } from './interfaces'; + +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: 'Bad Request', + }) + message: string; // Message for the exception + + @ApiProperty({ + description: 'A description of the error message.', + example: 'The input provided was invalid', + }) + 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/exceptions/index.ts b/src/core/exceptions/index.ts similarity index 55% rename from src/exceptions/index.ts rename to src/core/exceptions/index.ts index c8aedd9..853406b 100644 --- a/src/exceptions/index.ts +++ b/src/core/exceptions/index.ts @@ -1,4 +1,8 @@ +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 './unauthorized.exception'; -export * from './forbidden.exception'; diff --git a/src/exceptions/exceptions.interface.ts b/src/core/exceptions/interfaces/exceptions.interface.ts similarity index 68% rename from src/exceptions/exceptions.interface.ts rename to src/core/exceptions/interfaces/exceptions.interface.ts index 701038d..9e894f5 100644 --- a/src/exceptions/exceptions.interface.ts +++ b/src/core/exceptions/interfaces/exceptions.interface.ts @@ -36,3 +36,21 @@ export interface IHttpForbiddenExceptionResponse { timestamp: string; traceId: string; } + +export interface IHttpGatewayTimeOutExceptionResponse { + code: number; + message: string; + description?: string; + timestamp: string; + traceId: string; + path: string; +} + +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 97% rename from src/exceptions/internal-server-error.exception.ts rename to src/core/exceptions/internal-server-error.exception.ts index 5b7eaa7..a94d40d 100644 --- a/src/exceptions/internal-server-error.exception.ts +++ b/src/core/exceptions/internal-server-error.exception.ts @@ -1,9 +1,8 @@ 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 export class InternalServerErrorException extends HttpException { diff --git a/src/exceptions/unauthorized.exception.ts b/src/core/exceptions/unauthorized.exception.ts similarity index 98% rename from src/exceptions/unauthorized.exception.ts rename to src/core/exceptions/unauthorized.exception.ts index abe2f67..3a884db 100644 --- a/src/exceptions/unauthorized.exception.ts +++ b/src/core/exceptions/unauthorized.exception.ts @@ -6,9 +6,8 @@ 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. diff --git a/src/filters/all-exception.filter.ts b/src/core/filters/all-exception.filter.ts similarity index 100% rename from src/filters/all-exception.filter.ts rename to src/core/filters/all-exception.filter.ts diff --git a/src/filters/bad-request-exception.filter.ts b/src/core/filters/bad-request-exception.filter.ts similarity index 90% rename from src/filters/bad-request-exception.filter.ts rename to src/core/filters/bad-request-exception.filter.ts index 47a777f..7880bd7 100644 --- a/src/filters/bad-request-exception.filter.ts +++ b/src/core/filters/bad-request-exception.filter.ts @@ -1,7 +1,7 @@ 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`. @@ -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 88% rename from src/filters/forbidden-exception.filter.ts rename to src/core/filters/forbidden-exception.filter.ts index f7afa0f..bdcc8d1 100644 --- a/src/filters/forbidden-exception.filter.ts +++ b/src/core/filters/forbidden-exception.filter.ts @@ -1,7 +1,7 @@ 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 @@ -35,7 +35,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..51ac3f0 --- /dev/null +++ b/src/core/filters/gateway-timeout.exception.filter.ts @@ -0,0 +1,28 @@ +import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; + +import { GatewayTimeoutException, IHttpGatewayTimeOutExceptionResponse } from 'src/core/exceptions'; + +@Catch(GatewayTimeoutException) +export class GatewayTimeOutExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GatewayTimeoutException.name); + + constructor(private readonly httpAdapterHost: HttpAdapterHost) {} + + 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 71% rename from src/filters/index.ts rename to src/core/filters/index.ts index dacd3b2..fa65da9 100644 --- a/src/filters/index.ts +++ b/src/core/filters/index.ts @@ -1,6 +1,8 @@ -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 './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 90% rename from src/filters/internal-server-error-exception.filter.ts rename to src/core/filters/internal-server-error-exception.filter.ts index c4cede5..5bead4f 100644 --- a/src/filters/internal-server-error-exception.filter.ts +++ b/src/core/filters/internal-server-error-exception.filter.ts @@ -2,7 +2,7 @@ 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`. @@ -43,7 +43,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 65% rename from src/filters/not-found-exception.filter.ts rename to src/core/filters/not-found-exception.filter.ts index 1abe4db..e381048 100644 --- a/src/filters/not-found-exception.filter.ts +++ b/src/core/filters/not-found-exception.filter.ts @@ -1,4 +1,4 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger, NotFoundException } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; /** @@ -22,7 +22,7 @@ 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. // In certain situations `httpAdapter` might not be available in the @@ -31,15 +31,18 @@ export class NotFoundExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); - const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const instanceException: NotFoundException = exception instanceof NotFoundException ? exception : new NotFoundException(exception); + + const httpStatus = exception instanceof NotFoundException ? exception.getStatus() : HttpStatus.NOT_FOUND; const request = ctx.getRequest(); // Construct the response body. - const responseBody = { - error: exception.code, - message: exception.message, - description: exception.description, + const responseBody: Record = { + code: httpStatus, + message: 'Not Found', + description: instanceException.message, + timestamp: new Date().toISOString(), traceId: request.id, }; diff --git a/src/filters/unauthorized-exception.filter.ts b/src/core/filters/unauthorized-exception.filter.ts similarity index 88% rename from src/filters/unauthorized-exception.filter.ts rename to src/core/filters/unauthorized-exception.filter.ts index ac5d2ae..f431390 100644 --- a/src/filters/unauthorized-exception.filter.ts +++ b/src/core/filters/unauthorized-exception.filter.ts @@ -1,7 +1,7 @@ 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 @@ -35,7 +35,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/filters/validator-exception.filter.ts b/src/core/filters/validator-exception.filter.ts similarity index 99% rename from src/filters/validator-exception.filter.ts rename to src/core/filters/validator-exception.filter.ts index 3d9d620..0452068 100644 --- a/src/filters/validator-exception.filter.ts +++ b/src/core/filters/validator-exception.filter.ts @@ -1,5 +1,6 @@ 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'; diff --git a/src/core/interceptors/index.ts b/src/core/interceptors/index.ts new file mode 100644 index 0000000..2c59527 --- /dev/null +++ b/src/core/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './serializer.interceptor'; +export * from './timeout.interceptor'; diff --git a/src/core/interceptors/serializer.interceptor.ts b/src/core/interceptors/serializer.interceptor.ts new file mode 100644 index 0000000..154b5e6 --- /dev/null +++ b/src/core/interceptors/serializer.interceptor.ts @@ -0,0 +1,13 @@ +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 { + 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..b3cdc93 --- /dev/null +++ b/src/core/interceptors/timeout.interceptor.ts @@ -0,0 +1,28 @@ +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'; + +@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 index 9dfd4d9..bb5a432 100644 --- a/src/core/log/log.module.ts +++ b/src/core/log/log.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IncomingMessage, ServerResponse } from 'http'; @@ -7,6 +7,8 @@ import { LoggerModule } from 'nestjs-pino'; import { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from 'src/config/app.config'; +import { RequestLoggerMiddleware } from '../middlewares'; + @Module({ imports: [ // Configure logging @@ -58,6 +60,7 @@ import { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from 'src/config/app.config'; } : null, }, + // forRoutes: ['*'], // Log all requests }; }, }), @@ -66,4 +69,9 @@ import { E_APP_ENVIRONMENTS, E_APP_LOG_LEVELS } from 'src/config/app.config'; providers: [], exports: [], }) -export class LogModule {} +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..80898e2 --- /dev/null +++ b/src/core/middlewares/logging.middleware.ts @@ -0,0 +1,22 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; + +import { NextFunction, Request, Response } from 'express'; + +import { randomUUID } from 'node:crypto'; + +@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/main.ts b/src/main.ts index c1f5b39..629e573 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ // Import external modules import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { ClassSerializerInterceptor, Logger, ValidationPipe } from '@nestjs/common'; -import { NestFactory, Reflector } from '@nestjs/core'; +import { Logger, ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import { ExpressAdapter, NestExpressApplication } from '@nestjs/platform-express'; import * as cluster from 'cluster'; @@ -31,12 +31,15 @@ export async function bootstrap(): Promise { bufferLogs: 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(); + + // Use the Pino logger for the application + app.useLogger(app.get(Pino)); + // Get configuration service from the application const configService: ConfigService = app.get(ConfigService); @@ -94,15 +97,6 @@ export async function bootstrap(): Promise { // Set up the validation pipe app.useGlobalPipes(new ValidationPipe(validationOptionsUtil)); - // Set the global interceptors - app.useGlobalInterceptors( - // Use the class serializer interceptor to serialize the response objects - new ClassSerializerInterceptor(app.get(Reflector)), - ); - - // Allow all origins - app.enableCors(); - // Check if Swagger is enabled if (swaggerEnabled) { // Define the Swagger options and document diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e50ab74..5a25a48 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,13 +1,12 @@ import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { Body, Controller, HttpCode, Post, ValidationPipe } from '@nestjs/common'; +import { Body, Controller, HttpCode, Post } from '@nestjs/common'; + +import { BadRequestException, InternalServerErrorException, UnauthorizedException } from 'src/core/exceptions'; 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, }) @@ -27,8 +26,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 +37,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 ecd7df1..80ab36f 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -9,8 +9,7 @@ import { WorkspaceModule } from '../workspace/workspace.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; - -import { JwtUserStrategy } from './strategies/jwt-user.strategy'; +import { JwtUserStrategy } from './strategies'; @Module({ imports: [ diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 3b7abfd..504f22e 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -6,16 +6,14 @@ import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcryptjs'; import { generateOTPCode } from 'src/utils'; +import { BadRequestException, UnauthorizedException } from 'src/core/exceptions'; import { UserQueryService } from '../user/user.query.service'; import { User } from '../user/user.schema'; import { WorkspaceQueryService } from '../workspace/workspace.query-service'; -import { BadRequestException } from '../../exceptions/bad-request.exception'; -import { UnauthorizedException } from '../../exceptions/unauthorized.exception'; import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; - -import { JwtUserPayload } from './interfaces/jwt-user-payload.interface'; +import { JwtUserPayload } from './interfaces'; @Injectable() export class AuthService { @@ -48,7 +46,7 @@ export class AuthService { const userPayload: User = { email, password: hashedPassword, - workspace: workspace._id, + workspace: workspace, name, verified: true, registerCode: generateOTPCode(), 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/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 c2928c0..ac7c217 100644 --- a/src/modules/auth/strategies/jwt-user.strategy.ts +++ b/src/modules/auth/strategies/jwt-user.strategy.ts @@ -4,9 +4,10 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { JwtUserPayload } from '../interfaces/jwt-user-payload.interface'; +import { UnauthorizedException } from 'src/core/exceptions'; + +import { JwtUserPayload } from '../interfaces'; import { UserQueryService } from '../../user/user.query.service'; -import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; @Injectable() export class JwtUserStrategy extends PassportStrategy(Strategy, 'authUser') { diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 91b072b..8b39226 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -2,17 +2,13 @@ import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Controller, Get, HttpCode, Logger, UseGuards } from '@nestjs/common'; -// Internal dependencies +import { GetUser } from '../auth/decorators'; + import { GetProfileResDto } from './dtos'; import { UserDocument } from './user.schema'; -// Other modules dependencies -import { GetUser } from '../auth/decorators/get-user.decorator'; -import { JwtUserAuthGuard } from '../auth/guards/jwt-user-auth.guard'; - @ApiBearerAuth() @ApiTags('User') -@UseGuards(JwtUserAuthGuard) @Controller('user') export class UserController { private readonly logger = new Logger(UserController.name); diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 94d89da..1bc3325 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,7 +1,8 @@ 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'; diff --git a/src/modules/user/user.query.service.ts b/src/modules/user/user.query.service.ts index dd25f94..d76a41a 100644 --- a/src/modules/user/user.query.service.ts +++ b/src/modules/user/user.query.service.ts @@ -2,15 +2,11 @@ // 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'; @Injectable() export class UserQueryService { diff --git a/src/modules/user/user.repository.ts b/src/modules/user/user.repository.ts index 0fdbb21..3b6b122 100644 --- a/src/modules/user/user.repository.ts +++ b/src/modules/user/user.repository.ts @@ -1,29 +1,31 @@ // 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'; @Injectable() -export class UserRepository { - constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model) {} +export class UserRepository extends DatabaseAbstractRepository { + constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model) { + super(userModel); + } async find(filter: FilterQuery): Promise { - return this.userModel.find(filter).lean(); + return await this.userModel.find(filter); } async findById(id: string | Types.ObjectId): Promise { - return this.userModel.findById(id).lean(); + return await this.userModel.findById(id); } async findOne(filter: FilterQuery): Promise { - return this.userModel.findOne(filter).lean(); + return await this.userModel.findOne(filter); } async create(user: User): Promise { diff --git a/src/modules/user/user.schema.ts b/src/modules/user/user.schema.ts index 4a37044..539ccd7 100644 --- a/src/modules/user/user.schema.ts +++ b/src/modules/user/user.schema.ts @@ -1,34 +1,31 @@ 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; +@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 +42,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 +61,7 @@ export class User { example: true, }) @Prop({ - type: MongooseSchema.Types.Boolean, + type: Boolean, default: false, }) verified: boolean; @@ -70,14 +69,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 +87,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/workspace.module.ts b/src/modules/workspace/workspace.module.ts index ddeb16d..2a1e291 100644 --- a/src/modules/workspace/workspace.module.ts +++ b/src/modules/workspace/workspace.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; +import { DatabaseCollectionNames } from 'src/shared'; + import { WorkspaceQueryService } from './workspace.query-service'; import { WorkspaceRepository } from './workspace.repository'; import { WorkspaceSchema } from './workspace.schema'; diff --git a/src/modules/workspace/workspace.query-service.ts b/src/modules/workspace/workspace.query-service.ts index cc659a3..9433b82 100644 --- a/src/modules/workspace/workspace.query-service.ts +++ b/src/modules/workspace/workspace.query-service.ts @@ -1,9 +1,9 @@ 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'; @Injectable() export class WorkspaceQueryService { diff --git a/src/modules/workspace/workspace.repository.ts b/src/modules/workspace/workspace.repository.ts index c18a809..ccc0392 100644 --- a/src/modules/workspace/workspace.repository.ts +++ b/src/modules/workspace/workspace.repository.ts @@ -1,28 +1,33 @@ -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 { + constructor(@InjectModel(DatabaseCollectionNames.WORKSPACE) private workspaceModel: Model) { + super(workspaceModel); + } - async find(filter: FilterQuery, selectOptions?: ProjectionType): Promise { - return this.workspaceModel.find(filter, selectOptions).lean(); + async find(filter: FilterQuery, selectOptions?: ProjectionType): Promise { + return this.workspaceModel.find(filter, selectOptions); } - async findOne(filter: FilterQuery): Promise { - return this.workspaceModel.findOne(filter).lean(); + async findOne(filter: FilterQuery): Promise { + return this.workspaceModel.findOne(filter); } - async create(workspace: Workspace): Promise { + async create(workspace: Workspace): Promise { return this.workspaceModel.create(workspace); } - async findById(workspaceId: string): Promise { - return this.workspaceModel.findById(workspaceId).lean(); + async findById(workspaceId: string): Promise { + return this.workspaceModel.findById(workspaceId); } async findOneAndUpdate( @@ -30,10 +35,10 @@ export class WorkspaceRepository { 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(); + 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..1db95a3 100644 --- a/src/modules/workspace/workspace.schema.ts +++ b/src/modules/workspace/workspace.schema.ts @@ -1,49 +1,40 @@ 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 { - @ApiProperty({ - description: 'The unique identifier of the workspace', - example: '507f191e810c19729de860ea', - }) - @Prop({ - type: MongooseSchema.Types.ObjectId, - default: () => new Types.ObjectId(), - }) - _id?: Types.ObjectId; +import { DatabaseCollectionNames } from 'src/shared'; +import { getDatabaseSchemaOptions } from 'src/core/database/database-schema-options'; +import { EntityDocumentHelper } from 'src/utils'; + +export type WorkspaceDocument = HydratedDocument; +@Schema(getDatabaseSchemaOptions(DatabaseCollectionNames.WORKSPACE)) +export class Workspace extends EntityDocumentHelper { @ApiProperty({ + type: String, description: 'The name of the workspace', example: 'My Workspace', }) @Prop({ - type: MongooseSchema.Types.String, + type: String, required: 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/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..968a286 --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,2 @@ +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..cafa1dd --- /dev/null +++ b/src/shared/types/or-never.type.ts @@ -0,0 +1 @@ +export type OrNeverType = T | never; diff --git a/src/utils/date-time.util.ts b/src/utils/date-time.util.ts new file mode 100644 index 0000000..18385d6 --- /dev/null +++ b/src/utils/date-time.util.ts @@ -0,0 +1,17 @@ +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..31e1784 --- /dev/null +++ b/src/utils/deep-resolver.util.ts @@ -0,0 +1,28 @@ +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..7c712dd --- /dev/null +++ b/src/utils/document-entity-helper.util.ts @@ -0,0 +1,42 @@ +import { Prop } from '@nestjs/mongoose'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +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 index 58740ba..288a56d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,6 @@ +export * from './date-time.util'; +export * from './deep-resolver.util'; +export * from './document-entity-helper.util'; export * from './number.util'; export * from './validate-config.util'; export * from './validation-options.util'; diff --git a/src/utils/validation-options.util.ts b/src/utils/validation-options.util.ts index 416fa08..23b3c81 100644 --- a/src/utils/validation-options.util.ts +++ b/src/utils/validation-options.util.ts @@ -23,17 +23,11 @@ import { HttpStatus, UnprocessableEntityException, ValidationError, ValidationPi * }, * ]); */ -const generateErrors = (errors: ValidationError[]): unknown => { - return errors.reduce( - (accumulator, currentValue) => ({ - ...accumulator, - [currentValue.property]: - (currentValue.children?.length ?? 0) > 0 - ? generateErrors(currentValue.children ?? []) - : Object.values(currentValue.constraints ?? {}).join(', '), - }), - {}, - ); +const generateErrors = (errors: ValidationError[]) => { + return errors.map((error) => ({ + property: error.property, + errors: (error.children?.length ?? 0) > 0 ? generateErrors(error.children ?? []) : Object.values(error.constraints ?? []), + })); }; /** @@ -41,13 +35,20 @@ const generateErrors = (errors: ValidationError[]): unknown => { * @description This is the validation options util. */ export const validationOptionsUtil: ValidationPipeOptions = { - transform: true, - whitelist: true, - errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + 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({ - status: HttpStatus.UNPROCESSABLE_ENTITY, - errors: generateErrors(errors), + validationErrors: generateErrors(errors), }); }, }; From bafc349f0619bd1354d1c44632509d47062359d2 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 17:41:49 -0500 Subject: [PATCH 4/9] feat(core): enhance application structure and error handling - Updated the example.env file: - Added a REQUEST_TIMEOUT variable with a default value of 30 seconds, to specify the request timeout in milliseconds. - Refactored the app.module.ts file: - Removed unused imports and commented code. - Injected the ConfigService dependency to use the 'infra.requestTimeout' configuration value. - Passed the timeoutInMilliseconds value to the TimeoutInterceptor class. - Updated the infra.config.ts file: - Added a REQUEST_TIMEOUT variable with a default value of 30 seconds, to specify the request timeout in milliseconds. - Created the modules.module.ts file: - Added the ModulesModule. - Imported the AuthModule, UserModule, and WorkspaceModule. --- example.env | 3 ++- src/app.module.ts | 25 ++++++++++--------------- src/config/infra.config.ts | 9 ++++++++- src/modules/auth/auth.module.ts | 4 ++-- src/modules/modules.module.ts | 13 +++++++++++++ 5 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 src/modules/modules.module.ts diff --git a/example.env b/example.env index 56070ef..d49aa86 100644 --- a/example.env +++ b/example.env @@ -15,7 +15,8 @@ API_PREFIX = "api" # The API prefix # ========================================================================================= # INFRA CONFIGURATION # ========================================================================================= -CLUSTERING = false # Whether to enable clustering mode for the application (true or false) +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 diff --git a/src/app.module.ts b/src/app.module.ts index 079a99d..df22a84 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,30 +1,23 @@ import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { CoreModule } from './core/core.module'; import { AllExceptionsFilter, BadRequestExceptionFilter, - NotFoundExceptionFilter, - UnauthorizedExceptionFilter, ForbiddenExceptionFilter, GatewayTimeOutExceptionFilter, + NotFoundExceptionFilter, + UnauthorizedExceptionFilter, ValidationExceptionFilter, } from './core/filters'; import { TimeoutInterceptor } from './core/interceptors'; -import { AuthModule } from './modules/auth/auth.module'; +import { ModulesModule } from './modules/modules.module'; import { JwtUserAuthGuard } from './modules/auth/guards'; -import { UserModule } from './modules/user/user.module'; -import { WorkspaceModule } from './modules/workspace/workspace.module'; @Module({ - imports: [ - CoreModule, - // Import other modules - AuthModule, - UserModule, - WorkspaceModule, - ], + imports: [CoreModule, ModulesModule], providers: [ { provide: APP_FILTER, @@ -56,11 +49,13 @@ import { WorkspaceModule } from './modules/workspace/workspace.module'; }, { provide: APP_INTERCEPTOR, - useFactory: () => { - // TODO: Move this to config - const timeoutInMilliseconds = 30000; + useFactory: (configService: ConfigService) => { + const timeoutInMilliseconds = configService.get('infra.requestTimeout', { + infer: true, + }); return new TimeoutInterceptor(timeoutInMilliseconds); }, + inject: [ConfigService], }, { provide: APP_GUARD, diff --git a/src/config/infra.config.ts b/src/config/infra.config.ts index 9723f6e..755a533 100644 --- a/src/config/infra.config.ts +++ b/src/config/infra.config.ts @@ -1,22 +1,29 @@ import { registerAs } from '@nestjs/config'; -import { IsBoolean, IsNotEmpty } from 'class-validator'; +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; } 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/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 80ab36f..ee47d2d 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -32,8 +32,8 @@ import { JwtUserStrategy } from './strategies'; UserModule, WorkspaceModule, ], - providers: [ConfigService, JwtUserStrategy, AuthService], controllers: [AuthController], - exports: [JwtUserStrategy, PassportModule], + providers: [ConfigService, JwtUserStrategy, AuthService], + exports: [JwtUserStrategy, PassportModule, AuthService], }) export class AuthModule {} diff --git a/src/modules/modules.module.ts b/src/modules/modules.module.ts new file mode 100644 index 0000000..2fb2aed --- /dev/null +++ b/src/modules/modules.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { AuthModule } from './auth/auth.module'; +import { UserModule } from './user/user.module'; +import { WorkspaceModule } from './workspace/workspace.module'; + +@Module({ + imports: [AuthModule, UserModule, WorkspaceModule], + controllers: [], + providers: [], + exports: [], +}) +export class ModulesModule {} From 665299d35a8355141e245a1dcf499ebe2c44d1a8 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 19:04:51 -0500 Subject: [PATCH 5/9] feat: swc compiler and api errors responses decorator feat(shared): add API error response decorator module chore(core): enhance application structure and add error handling chore(babel): update SWC dependencies and compiler options --- nest-cli.json | 2 + package-lock.json | 1456 +++++++++++++++++ package.json | 2 + src/core/application/application.service.ts | 2 +- src/core/exceptions/forbidden.exception.ts | 5 +- .../exceptions/gateway-timeout.exception.ts | 4 +- src/metadata.ts | 10 + src/modules/auth/auth.controller.ts | 14 +- src/modules/user/user.controller.ts | 3 + .../api-error-responses.decorator.ts | 41 + src/shared/decorators/index.ts | 1 + src/shared/index.ts | 1 + 12 files changed, 1525 insertions(+), 16 deletions(-) create mode 100644 src/metadata.ts create mode 100644 src/shared/decorators/api-error-responses.decorator.ts create mode 100644 src/shared/decorators/index.ts diff --git a/nest-cli.json b/nest-cli.json index fe4f69b..fedc9f3 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -4,6 +4,8 @@ "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, + "builder": "swc", + "typeCheck": true, "plugins": [ { "name": "@nestjs/swagger", diff --git a/package-lock.json b/package-lock.json index d24edaf..3fcad18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,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", @@ -1878,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", @@ -1886,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", @@ -2314,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", @@ -2332,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", @@ -2407,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", @@ -2481,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", @@ -2542,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", @@ -2587,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", @@ -3223,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", @@ -3570,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", @@ -3776,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", @@ -4097,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", @@ -4475,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", @@ -4516,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", @@ -5442,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", @@ -5526,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", @@ -5653,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", @@ -5686,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", @@ -5744,6 +6825,22 @@ "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": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -6303,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", @@ -6460,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", @@ -6475,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", @@ -8623,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", @@ -8815,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", @@ -9135,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", @@ -9333,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", @@ -9342,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", @@ -9529,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", @@ -9559,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", @@ -9755,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", @@ -9938,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", @@ -10205,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", @@ -10316,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", @@ -10358,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", @@ -10580,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", @@ -10824,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", @@ -11098,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", @@ -11131,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", @@ -11462,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", @@ -11491,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 11bd720..2046efa 100644 --- a/package.json +++ b/package.json @@ -58,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", diff --git a/src/core/application/application.service.ts b/src/core/application/application.service.ts index c3b8f97..8218c02 100644 --- a/src/core/application/application.service.ts +++ b/src/core/application/application.service.ts @@ -3,6 +3,6 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class ApplicationService { getHello(): string { - return 'Yeah yeah! we are okay!'; + return 'Yeah yeah! we are okay! :)'; } } diff --git a/src/core/exceptions/forbidden.exception.ts b/src/core/exceptions/forbidden.exception.ts index c6d64c4..a106386 100644 --- a/src/core/exceptions/forbidden.exception.ts +++ b/src/core/exceptions/forbidden.exception.ts @@ -16,7 +16,7 @@ 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; @@ -28,13 +28,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 index 506ccd5..0254482 100644 --- a/src/core/exceptions/gateway-timeout.exception.ts +++ b/src/core/exceptions/gateway-timeout.exception.ts @@ -17,13 +17,13 @@ export class GatewayTimeoutException extends HttpException { @ApiProperty({ description: 'Message for the exception', - example: 'Bad Request', + example: 'Gateway Timeout', }) message: string; // Message for the exception @ApiProperty({ description: 'A description of the error message.', - example: 'The input provided was invalid', + example: 'The server is taking too long to respond.', }) description?: string; // Description of the exception diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 0000000..4a101f3 --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,10 @@ +/* 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/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("./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/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 } } }], [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 5a25a48..3882d0d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,21 +1,13 @@ -import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Body, Controller, HttpCode, Post } from '@nestjs/common'; -import { BadRequestException, InternalServerErrorException, UnauthorizedException } from 'src/core/exceptions'; +import { ApiErrorResponses } from 'src/shared'; import { AuthService } from './auth.service'; import { Public } from './decorators'; import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; -@ApiBadRequestResponse({ - type: BadRequestException, -}) -@ApiInternalServerErrorResponse({ - type: InternalServerErrorException, -}) -@ApiUnauthorizedResponse({ - type: UnauthorizedException, -}) +@ApiErrorResponses() @ApiTags('Auth') @Controller('auth') export class AuthController { diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 8b39226..8faf5de 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -2,12 +2,15 @@ import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Controller, Get, HttpCode, Logger, UseGuards } from '@nestjs/common'; +import { ApiErrorResponses } from 'src/shared'; + import { GetUser } from '../auth/decorators'; import { GetProfileResDto } from './dtos'; import { UserDocument } from './user.schema'; @ApiBearerAuth() +@ApiErrorResponses() @ApiTags('User') @Controller('user') export class UserController { 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..18dbfda --- /dev/null +++ b/src/shared/decorators/api-error-responses.decorator.ts @@ -0,0 +1,41 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiGatewayTimeoutResponse, + ApiInternalServerErrorResponse, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; + +import { + BadRequestException, + ForbiddenException, + GatewayTimeoutException, + InternalServerErrorException, + UnauthorizedException, +} from 'src/core/exceptions'; + +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/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..d16eb17 --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +1 @@ +export * from './api-error-responses.decorator'; diff --git a/src/shared/index.ts b/src/shared/index.ts index 968a286..6310436 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,2 +1,3 @@ +export * from './decorators'; export * from './enums'; export * from './types'; From 2952063894d49b575b0931f890349ecb2427616b Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sat, 5 Oct 2024 20:54:31 -0500 Subject: [PATCH 6/9] chore: updated readme --- .vscode/extensions.json | 3 + .vscode/settings.json | 3 + README.md | 327 +++++++++++++++++++++++----------------- src/main.ts | 1 + 4 files changed, 198 insertions(+), 136 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..71fd58f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["shinotatwu-ds.file-tree-generator"] +} 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 8bfa701..44e6709 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a starter kit for building a Nest.js application with MongoDB, Express, To get started with this project, clone the repository and install the dependencies: -``` shell +```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 @@ -14,7 +14,7 @@ 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: -``` shell +```shell cp example.env .env # OR .env.local - .env.development - .env.test ``` @@ -22,34 +22,35 @@ cp example.env .env # OR .env.local - .env.development - .env.test 1. Generate a private key: - ``` shell - openssl genrsa -out .keys/private_key.pem 2048 - ``` + ```shell + openssl genrsa -out .keys/private_key.pem 2048 + ``` -2. Extract the public key from the private key: +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 - ``` + ```shell + openssl rsa -in .keys/private_key.pem -outform PEM -pubout -out .keys/public_key.pem + ``` - 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). **DONT SHARE THE PRIVATE KEY WITH ANYONE.** + 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: + **_NOTE:_** The key size (2048 bits in this example) can be adjusted to suit your security requirements. - ```shell - openssl base64 -A -in .keys/public_key.pem -out .keys/public_key_base64.txt - ``` +1. Encode the public key in base64 encoding: - Copy the contents of the `.keys/public_key_base64.txt` file and paste it into `JWT_PUBLIC_KEY` variable declared on your `.env` file. + ```shell + openssl base64 -A -in .keys/public_key.pem -out .keys/public_key_base64.txt + ``` -4. Encode the private key in base64 encoding: + 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. - ```sh - openssl base64 -A -in .keys/private_key.pem -out .keys/private_key_base64.txt - ``` +1. Encode the private key in base64 encoding: - Copy the contents of the `.keys/private_key_base64.txt` file and paste it into `JWT_PUBLJWT_PRIVATE_KEYIC_KEY` variable declared on your `.env` file. + ```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 @@ -91,114 +92,163 @@ 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 -├── .keys/ -│ ├── .gitkeep -├── src -│   ├── app.controller.spec.ts -│   ├── app.controller.ts -│   ├── app.module.ts -│   ├── app.service.ts -│   ├── config -│   │   ├── api.config.ts -│   │   ├── app.config.ts -│   │   ├── auth.config.ts -│   │   ├── database.config.ts -│   │   ├── infra.config.ts -│   │   └── swagger.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.tsFilter -│   │   ├── 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 -│   └── types -│   ├── index.ts -│   └── schema.type.ts -│   └── utils -│   ├── index.ts -│   ├── number.util.ts -│   └── validation-options.util.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. @@ -212,24 +262,29 @@ This project follows a structured organization to maintain clean, scalable code, ### 2. Source Code (`src/`) -- **`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`, `auth.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. +- **`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`) used across the application. - - **`types/`**: Custom TypeScript types (e.g., `schema.type.ts`) used for type safety throughout the codebase. -- **`utils/`**: Utility functions (e.g., `number.util.ts`, `validation-options.util.ts`) for common operations. + - **`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/src/main.ts b/src/main.ts index 629e573..697a2d4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,6 +29,7 @@ export async function bootstrap(): Promise { // Create the NestJS application instance const app: NestExpressApplication = await NestFactory.create(AppModule, new ExpressAdapter(), { bufferLogs: true, + forceCloseConnections: true, }); // Use the application container From 71acb1412e05f46ad988a35a424d3927c0632dc8 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Sun, 6 Oct 2024 01:55:46 -0500 Subject: [PATCH 7/9] fix: code documentation and fix validation exception --- .vscode/extensions.json | 3 - src/app.module.ts | 31 +++++ src/config/api.config.ts | 12 ++ src/config/app.config.ts | 11 ++ src/config/auth.config.ts | 13 ++ src/config/database.config.ts | 10 ++ src/config/infra.config.ts | 14 +++ src/config/swagger.config.ts | 14 +++ .../application/application.controller.ts | 17 +++ src/core/application/application.module.ts | 26 ++++ src/core/application/application.service.ts | 8 ++ src/core/core.module.ts | 7 ++ .../abstracts/database.abstract.interface.ts | 7 ++ .../abstracts/database.abstract.repository.ts | 41 +++++++ src/core/database/database-schema-options.ts | 17 +++ src/core/database/database.module.ts | 18 +++ src/core/database/database.service.ts | 26 +++- src/core/exceptions/all.exception.ts | 22 ++++ src/core/exceptions/bad-request.exception.ts | 112 +++++++++--------- .../constants/exceptions.constants.ts | 50 +++++++- src/core/exceptions/forbidden.exception.ts | 40 +++++++ .../exceptions/gateway-timeout.exception.ts | 27 +++++ .../interfaces/exceptions.interface.ts | 76 ++++++++++++ .../internal-server-error.exception.ts | 34 +++++- src/core/exceptions/unauthorized.exception.ts | 60 ++++++++++ src/core/filters/all-exception.filter.ts | 75 +++++++----- .../filters/bad-request-exception.filter.ts | 4 +- .../filters/forbidden-exception.filter.ts | 14 ++- .../gateway-timeout.exception.filter.ts | 16 +++ .../internal-server-error-exception.filter.ts | 5 + .../filters/not-found-exception.filter.ts | 5 + .../filters/unauthorized-exception.filter.ts | 12 +- .../filters/validator-exception.filter.ts | 12 +- .../interceptors/serializer.interceptor.ts | 7 ++ src/core/interceptors/timeout.interceptor.ts | 16 +++ src/core/log/log.module.ts | 66 +++++++++++ src/core/middlewares/logging.middleware.ts | 21 ++++ src/main.ts | 28 ++++- src/modules/modules.module.ts | 5 + src/modules/user/dtos/get-profile.res.dto.ts | 3 + src/modules/user/user.controller.ts | 16 +++ src/modules/user/user.module.ts | 8 ++ src/modules/user/user.query.service.ts | 27 ++++- src/modules/user/user.repository.ts | 47 +++++++- src/modules/user/user.schema.ts | 17 +++ src/modules/workspace/workspace.module.ts | 20 ++++ .../workspace/workspace.query-service.ts | 23 ++++ src/modules/workspace/workspace.repository.ts | 44 +++++++ src/modules/workspace/workspace.schema.ts | 13 ++ .../api-error-responses.decorator.ts | 12 ++ src/shared/enums/db.enum.ts | 7 ++ src/shared/types/or-never.type.ts | 26 ++++ src/shared/types/schema.type.ts | 10 ++ src/utils/date-time.util.ts | 13 ++ src/utils/deep-resolver.util.ts | 21 ++++ src/utils/document-entity-helper.util.ts | 11 ++ src/utils/number.util.ts | 29 +++-- src/utils/validate-config.util.ts | 11 +- src/utils/validation-options.util.ts | 20 ++++ 59 files changed, 1232 insertions(+), 128 deletions(-) delete mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 71fd58f..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["shinotatwu-ds.file-tree-generator"] -} diff --git a/src/app.module.ts b/src/app.module.ts index df22a84..a08754e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,3 +1,7 @@ +/** + * 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'; @@ -16,6 +20,33 @@ 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: [CoreModule, ModulesModule], providers: [ diff --git a/src/config/api.config.ts b/src/config/api.config.ts index 1433b1f..e4ab654 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -1,3 +1,6 @@ +/** + * 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'; @@ -20,6 +23,15 @@ class EnvironmentVariablesValidator { 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); diff --git a/src/config/app.config.ts b/src/config/app.config.ts index b0ef632..b9a8011 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -46,6 +46,17 @@ class EnvironmentVariablesValidator { 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); diff --git a/src/config/auth.config.ts b/src/config/auth.config.ts index 7ae8cc5..cc83344 100644 --- a/src/config/auth.config.ts +++ b/src/config/auth.config.ts @@ -31,6 +31,19 @@ class EnvironmentVariablesValidator { 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 { diff --git a/src/config/database.config.ts b/src/config/database.config.ts index af6c756..3b9ab4a 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -31,6 +31,16 @@ class EnvironmentVariablesValidator { MONGODB_HEARTBEAT_FREQUENCY_MS: number; } +/** + * 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); diff --git a/src/config/infra.config.ts b/src/config/infra.config.ts index 755a533..cf81783 100644 --- a/src/config/infra.config.ts +++ b/src/config/infra.config.ts @@ -20,6 +20,20 @@ class EnvironmentVariablesValidator { 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 { diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index 38ffa0b..c333cc2 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -44,6 +44,20 @@ class EnvironmentVariablesValidator { 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 { diff --git a/src/core/application/application.controller.ts b/src/core/application/application.controller.ts index cd1bfeb..0193c51 100644 --- a/src/core/application/application.controller.ts +++ b/src/core/application/application.controller.ts @@ -5,6 +5,23 @@ 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 { diff --git a/src/core/application/application.module.ts b/src/core/application/application.module.ts index 73f01a0..b0333b6 100644 --- a/src/core/application/application.module.ts +++ b/src/core/application/application.module.ts @@ -14,6 +14,32 @@ 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 diff --git a/src/core/application/application.service.ts b/src/core/application/application.service.ts index 8218c02..d5cb296 100644 --- a/src/core/application/application.service.ts +++ b/src/core/application/application.service.ts @@ -1,7 +1,15 @@ 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 index 2598072..831cbff 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -2,6 +2,13 @@ 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: [], diff --git a/src/core/database/abstracts/database.abstract.interface.ts b/src/core/database/abstracts/database.abstract.interface.ts index d0998e8..d7d5a92 100644 --- a/src/core/database/abstracts/database.abstract.interface.ts +++ b/src/core/database/abstracts/database.abstract.interface.ts @@ -1,3 +1,10 @@ +/** + * 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; diff --git a/src/core/database/abstracts/database.abstract.repository.ts b/src/core/database/abstracts/database.abstract.repository.ts index f56737f..b83d4e0 100644 --- a/src/core/database/abstracts/database.abstract.repository.ts +++ b/src/core/database/abstracts/database.abstract.repository.ts @@ -2,17 +2,43 @@ 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) { @@ -21,6 +47,14 @@ export abstract class DatabaseAbstractRepository { 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) { @@ -29,6 +63,13 @@ export abstract class DatabaseAbstractRepository { 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) { diff --git a/src/core/database/database-schema-options.ts b/src/core/database/database-schema-options.ts index 20080ef..920b793 100644 --- a/src/core/database/database-schema-options.ts +++ b/src/core/database/database-schema-options.ts @@ -1,5 +1,22 @@ 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, diff --git a/src/core/database/database.module.ts b/src/core/database/database.module.ts index e4f55e4..246d58e 100644 --- a/src/core/database/database.module.ts +++ b/src/core/database/database.module.ts @@ -4,6 +4,24 @@ 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({ diff --git a/src/core/database/database.service.ts b/src/core/database/database.service.ts index d306e47..d9adeff 100644 --- a/src/core/database/database.service.ts +++ b/src/core/database/database.service.ts @@ -4,12 +4,36 @@ 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 { - private readonly logger = new Logger(DatabaseService.name); + /** + * 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 { this.logger.error('Creating Mongoose options...'); const autoPopulate: boolean = this.configService.get('database.autoPopulate', { diff --git a/src/core/exceptions/all.exception.ts b/src/core/exceptions/all.exception.ts index 6233092..9035b49 100644 --- a/src/core/exceptions/all.exception.ts +++ b/src/core/exceptions/all.exception.ts @@ -9,7 +9,14 @@ 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.', @@ -17,24 +24,36 @@ export class AllException extends HttpException { }) 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', @@ -42,6 +61,9 @@ export class AllException 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', diff --git a/src/core/exceptions/bad-request.exception.ts b/src/core/exceptions/bad-request.exception.ts index e385e7b..7912d78 100644 --- a/src/core/exceptions/bad-request.exception.ts +++ b/src/core/exceptions/bad-request.exception.ts @@ -1,58 +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 { 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, @@ -65,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, @@ -88,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', @@ -99,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', @@ -111,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', @@ -123,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', @@ -135,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', @@ -147,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 index 5a7aa88..11eedb6 100644 --- a/src/core/exceptions/constants/exceptions.constants.ts +++ b/src/core/exceptions/constants/exceptions.constants.ts @@ -1,5 +1,53 @@ /** - * This class defines constants for HTTP error codes. + * A class containing constants for various HTTP error codes. + * + * @class ExceptionConstants + * + * @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 { /** diff --git a/src/core/exceptions/forbidden.exception.ts b/src/core/exceptions/forbidden.exception.ts index a106386..d8d3469 100644 --- a/src/core/exceptions/forbidden.exception.ts +++ b/src/core/exceptions/forbidden.exception.ts @@ -12,6 +12,46 @@ 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({ diff --git a/src/core/exceptions/gateway-timeout.exception.ts b/src/core/exceptions/gateway-timeout.exception.ts index 0254482..74806ac 100644 --- a/src/core/exceptions/gateway-timeout.exception.ts +++ b/src/core/exceptions/gateway-timeout.exception.ts @@ -4,6 +4,33 @@ 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, diff --git a/src/core/exceptions/interfaces/exceptions.interface.ts b/src/core/exceptions/interfaces/exceptions.interface.ts index 9e894f5..2b4817a 100644 --- a/src/core/exceptions/interfaces/exceptions.interface.ts +++ b/src/core/exceptions/interfaces/exceptions.interface.ts @@ -1,3 +1,13 @@ +/** + * 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; @@ -5,6 +15,17 @@ export interface IException { description?: 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; @@ -13,6 +34,17 @@ export interface IHttpBadRequestExceptionResponse { traceId: 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} timestamp - The timestamp when the error occurred. + * @property {string} traceId - A unique identifier for tracing the error. + */ export interface IHttpInternalServerErrorExceptionResponse { code: number; message: string; @@ -21,6 +53,17 @@ export interface IHttpInternalServerErrorExceptionResponse { 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; @@ -29,6 +72,17 @@ export interface IHttpUnauthorizedExceptionResponse { 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; @@ -37,6 +91,17 @@ export interface IHttpForbiddenExceptionResponse { traceId: string; } +/** + * Interface representing the structure of a conflict exception response. + * + * @interface IHttpConflictExceptionResponse + * + * @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. + */ export interface IHttpGatewayTimeOutExceptionResponse { code: number; message: string; @@ -46,6 +111,17 @@ export interface IHttpGatewayTimeOutExceptionResponse { path: string; } +/** + * Interface representing the structure of a request timeout exception response. + * + * @interface IHttpRequestTimeoutExceptionResponse + * + * @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 {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; diff --git a/src/core/exceptions/internal-server-error.exception.ts b/src/core/exceptions/internal-server-error.exception.ts index a94d40d..8bbbbee 100644 --- a/src/core/exceptions/internal-server-error.exception.ts +++ b/src/core/exceptions/internal-server-error.exception.ts @@ -5,7 +5,14 @@ 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.', @@ -13,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: @@ -29,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', @@ -36,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', @@ -86,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({ @@ -99,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/unauthorized.exception.ts b/src/core/exceptions/unauthorized.exception.ts index 3a884db..7f606cc 100644 --- a/src/core/exceptions/unauthorized.exception.ts +++ b/src/core/exceptions/unauthorized.exception.ts @@ -12,6 +12,66 @@ 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 index a166aa3..826d348 100644 --- a/src/core/filters/all-exception.filter.ts +++ b/src/core/filters/all-exception.filter.ts @@ -1,51 +1,62 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, UnprocessableEntityException } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; +import { AllException, ExceptionConstants } from '../exceptions'; + /** - * Catches all exceptions thrown by the application and sends an appropriate HTTP response. + * A filter to catch all exceptions and format the 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} + * Method to handle caught exceptions. + * @param exception - The exception that was thrown. + * @param host - The arguments host. */ - catch(exception: any, host: ArgumentsHost): void { - // Log the exception. + catch(exception: unknown, host: ArgumentsHost): void { 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); + 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/core/filters/bad-request-exception.filter.ts b/src/core/filters/bad-request-exception.filter.ts index 7880bd7..b29f44d 100644 --- a/src/core/filters/bad-request-exception.filter.ts +++ b/src/core/filters/bad-request-exception.filter.ts @@ -8,7 +8,7 @@ import { BadRequestException, IHttpBadRequestExceptionResponse } from '../except */ @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. diff --git a/src/core/filters/forbidden-exception.filter.ts b/src/core/filters/forbidden-exception.filter.ts index bdcc8d1..187f267 100644 --- a/src/core/filters/forbidden-exception.filter.ts +++ b/src/core/filters/forbidden-exception.filter.ts @@ -4,12 +4,22 @@ import { HttpAdapterHost } from '@nestjs/core'; 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) {} /** diff --git a/src/core/filters/gateway-timeout.exception.filter.ts b/src/core/filters/gateway-timeout.exception.filter.ts index 51ac3f0..ce4061c 100644 --- a/src/core/filters/gateway-timeout.exception.filter.ts +++ b/src/core/filters/gateway-timeout.exception.filter.ts @@ -3,12 +3,28 @@ 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'); diff --git a/src/core/filters/internal-server-error-exception.filter.ts b/src/core/filters/internal-server-error-exception.filter.ts index 5bead4f..89e7afc 100644 --- a/src/core/filters/internal-server-error-exception.filter.ts +++ b/src/core/filters/internal-server-error-exception.filter.ts @@ -9,6 +9,11 @@ import { IHttpInternalServerErrorExceptionResponse, InternalServerErrorException */ @Catch(InternalServerErrorException) export class InternalServerErrorExceptionFilter implements ExceptionFilter { + /** + * @private + * @readonly + * @type {${1:*}} + */ private readonly logger = new Logger(InternalServerErrorException.name); /** diff --git a/src/core/filters/not-found-exception.filter.ts b/src/core/filters/not-found-exception.filter.ts index e381048..2cd0cc8 100644 --- a/src/core/filters/not-found-exception.filter.ts +++ b/src/core/filters/not-found-exception.filter.ts @@ -6,6 +6,11 @@ import { HttpAdapterHost } from '@nestjs/core'; */ @Catch(NotFoundException) export class NotFoundExceptionFilter implements ExceptionFilter { + /** + * @private + * @readonly + * @type {${1:*}} + */ private readonly logger = new Logger(NotFoundExceptionFilter.name); /** diff --git a/src/core/filters/unauthorized-exception.filter.ts b/src/core/filters/unauthorized-exception.filter.ts index f431390..7c3780b 100644 --- a/src/core/filters/unauthorized-exception.filter.ts +++ b/src/core/filters/unauthorized-exception.filter.ts @@ -4,12 +4,22 @@ import { HttpAdapterHost } from '@nestjs/core'; 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) {} /** diff --git a/src/core/filters/validator-exception.filter.ts b/src/core/filters/validator-exception.filter.ts index 0452068..19151af 100644 --- a/src/core/filters/validator-exception.filter.ts +++ b/src/core/filters/validator-exception.filter.ts @@ -6,12 +6,22 @@ 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/serializer.interceptor.ts b/src/core/interceptors/serializer.interceptor.ts index 154b5e6..13ff71c 100644 --- a/src/core/interceptors/serializer.interceptor.ts +++ b/src/core/interceptors/serializer.interceptor.ts @@ -7,6 +7,13 @@ 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 index b3cdc93..2da169f 100644 --- a/src/core/interceptors/timeout.interceptor.ts +++ b/src/core/interceptors/timeout.interceptor.ts @@ -5,6 +5,22 @@ 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) {} diff --git a/src/core/log/log.module.ts b/src/core/log/log.module.ts index bb5a432..f076dbd 100644 --- a/src/core/log/log.module.ts +++ b/src/core/log/log.module.ts @@ -9,6 +9,72 @@ 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 diff --git a/src/core/middlewares/logging.middleware.ts b/src/core/middlewares/logging.middleware.ts index 80898e2..87aead8 100644 --- a/src/core/middlewares/logging.middleware.ts +++ b/src/core/middlewares/logging.middleware.ts @@ -4,6 +4,27 @@ 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); diff --git a/src/main.ts b/src/main.ts index 697a2d4..157283a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -// Import external modules import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { Logger, ValidationPipe } from '@nestjs/common'; @@ -24,7 +23,32 @@ 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 main function +/** + * 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: NestExpressApplication = await NestFactory.create(AppModule, new ExpressAdapter(), { diff --git a/src/modules/modules.module.ts b/src/modules/modules.module.ts index 2fb2aed..f6dab2e 100644 --- a/src/modules/modules.module.ts +++ b/src/modules/modules.module.ts @@ -4,6 +4,11 @@ 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: [], 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 8faf5de..b94798f 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -9,6 +9,22 @@ import { GetUser } from '../auth/decorators'; import { GetProfileResDto } from './dtos'; import { 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') diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 1bc3325..59b2219 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -8,6 +8,14 @@ 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 d76a41a..0475d04 100644 --- a/src/modules/user/user.query.service.ts +++ b/src/modules/user/user.query.service.ts @@ -1,5 +1,3 @@ -// Objective: Implement the user query service to handle the user queries -// External dependencies import { Injectable } from '@nestjs/common'; import { InternalServerErrorException } from 'src/core/exceptions'; @@ -8,11 +6,20 @@ import { Identifier } from 'src/shared'; 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 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 }); @@ -21,7 +28,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); @@ -30,7 +42,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 3b6b122..db28140 100644 --- a/src/modules/user/user.repository.ts +++ b/src/modules/user/user.repository.ts @@ -10,28 +10,62 @@ 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 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 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 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 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, @@ -40,7 +74,18 @@ export class UserRepository extends DatabaseAbstractRepository { 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 539ccd7..0a61072 100644 --- a/src/modules/user/user.schema.ts +++ b/src/modules/user/user.schema.ts @@ -11,6 +11,23 @@ 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 diff --git a/src/modules/workspace/workspace.module.ts b/src/modules/workspace/workspace.module.ts index 2a1e291..737245d 100644 --- a/src/modules/workspace/workspace.module.ts +++ b/src/modules/workspace/workspace.module.ts @@ -7,6 +7,26 @@ 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. + * + * @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 }])], providers: [WorkspaceQueryService, WorkspaceRepository], diff --git a/src/modules/workspace/workspace.query-service.ts b/src/modules/workspace/workspace.query-service.ts index 9433b82..719f5b2 100644 --- a/src/modules/workspace/workspace.query-service.ts +++ b/src/modules/workspace/workspace.query-service.ts @@ -5,10 +5,25 @@ import { InternalServerErrorException } from 'src/core/exceptions'; 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,14 @@ export class WorkspaceQueryService { } } + /** + * 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); diff --git a/src/modules/workspace/workspace.repository.ts b/src/modules/workspace/workspace.repository.ts index ccc0392..089d99b 100644 --- a/src/modules/workspace/workspace.repository.ts +++ b/src/modules/workspace/workspace.repository.ts @@ -10,26 +10,64 @@ import { Workspace, WorkspaceDocument } from './workspace.schema'; @Injectable() 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); } + /** + * 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); } + /** + * 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); } + /** + * 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); } + /** + * 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 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, @@ -38,6 +76,12 @@ export class WorkspaceRepository extends DatabaseAbstractRepository { return this.workspaceModel.findByIdAndDelete(workspaceId); } diff --git a/src/modules/workspace/workspace.schema.ts b/src/modules/workspace/workspace.schema.ts index 1db95a3..487ee2d 100644 --- a/src/modules/workspace/workspace.schema.ts +++ b/src/modules/workspace/workspace.schema.ts @@ -9,6 +9,19 @@ 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({ diff --git a/src/shared/decorators/api-error-responses.decorator.ts b/src/shared/decorators/api-error-responses.decorator.ts index 18dbfda..1569af2 100644 --- a/src/shared/decorators/api-error-responses.decorator.ts +++ b/src/shared/decorators/api-error-responses.decorator.ts @@ -15,6 +15,18 @@ import { 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({ 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/types/or-never.type.ts b/src/shared/types/or-never.type.ts index cafa1dd..4df7f38 100644 --- a/src/shared/types/or-never.type.ts +++ b/src/shared/types/or-never.type.ts @@ -1 +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 index 18385d6..0795fe8 100644 --- a/src/utils/date-time.util.ts +++ b/src/utils/date-time.util.ts @@ -1,3 +1,16 @@ +/** + * 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 diff --git a/src/utils/deep-resolver.util.ts b/src/utils/deep-resolver.util.ts index 31e1784..f59a2f6 100644 --- a/src/utils/deep-resolver.util.ts +++ b/src/utils/deep-resolver.util.ts @@ -1,3 +1,24 @@ +/** + * 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; diff --git a/src/utils/document-entity-helper.util.ts b/src/utils/document-entity-helper.util.ts index 7c712dd..7e1b48e 100644 --- a/src/utils/document-entity-helper.util.ts +++ b/src/utils/document-entity-helper.util.ts @@ -1,6 +1,17 @@ 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, diff --git a/src/utils/number.util.ts b/src/utils/number.util.ts index 3d08f40..04ab054 100644 --- a/src/utils/number.util.ts +++ b/src/utils/number.util.ts @@ -1,21 +1,26 @@ /** - * Generates a random six digit OTP - * @param {number} min - the minimum value of the OTP (default is 100000) - * @param {number} max - the maximum value of the OTP (default is 999999) - * @returns {number} - returns the generated OTP - * @throws {Error} - throws an error if the minimum value is greater than the maximum value - * @throws {Error} - throws an error if the minimum or maximum values are negative - * @throws {Error} - throws an error if the minimum or maximum values are not six digits long - * @throws {Error} - throws an error if the minimum and maximum values are the same - * @throws {Error} - throws an error if the minimum or maximum values are greater than 999999 - * @throws {Error} - throws an error if the minimum or maximum values are less than 100000 - * @throws {Error} - throws an error if the minimum or maximum values are not numbers + * 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; - // Validate the input values if (min > max) { throw new Error('Minimum value cannot be greater than maximum value'); } else if (min < 0 || max < 0) { diff --git a/src/utils/validate-config.util.ts b/src/utils/validate-config.util.ts index 187e169..ec6f0d5 100644 --- a/src/utils/validate-config.util.ts +++ b/src/utils/validate-config.util.ts @@ -4,7 +4,16 @@ import { validateSync } from 'class-validator'; import { ClassConstructor } from 'class-transformer/types/interfaces'; -export function validateConfigUtil(config: Record, envVariablesClass: ClassConstructor) { +/** + * 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, }); diff --git a/src/utils/validation-options.util.ts b/src/utils/validation-options.util.ts index 23b3c81..bdaa7ca 100644 --- a/src/utils/validation-options.util.ts +++ b/src/utils/validation-options.util.ts @@ -34,6 +34,26 @@ const generateErrors = (errors: ValidationError[]) => { * 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 From 4581740ae3392e8eada72b1054142b74533bf326 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Wed, 9 Oct 2024 21:53:09 -0500 Subject: [PATCH 8/9] feat: workspaces module implementation * Use workspaces with slug to manage unique workspace per user * If workspace exist, not create new --- src/core/database/database.service.ts | 2 - src/main.ts | 12 ++++ src/metadata.ts | 3 +- src/modules/auth/auth.service.ts | 50 +++++++++----- src/modules/auth/dtos/login.req.dto.ts | 4 +- src/modules/auth/dtos/signup.req.dto.ts | 8 ++- src/modules/user/user.controller.ts | 19 +++++- src/modules/user/user.query.service.ts | 13 ++++ src/modules/workspace/dtos/index.ts | 2 + .../workspace/dtos/workspace-req.dto.ts | 40 +++++++++++ .../workspace/dtos/workspace-res.dto.ts | 35 ++++++++++ src/modules/workspace/workspace.controller.ts | 67 +++++++++++++++++++ src/modules/workspace/workspace.module.ts | 5 ++ .../workspace/workspace.query-service.ts | 31 +++++++++ src/modules/workspace/workspace.repository.ts | 10 +++ src/modules/workspace/workspace.schema.ts | 14 ++++ src/utils/index.ts | 1 + src/utils/string.util.ts | 13 ++++ 18 files changed, 305 insertions(+), 24 deletions(-) create mode 100644 src/modules/workspace/dtos/index.ts create mode 100644 src/modules/workspace/dtos/workspace-req.dto.ts create mode 100644 src/modules/workspace/dtos/workspace-res.dto.ts create mode 100644 src/modules/workspace/workspace.controller.ts create mode 100644 src/utils/string.util.ts diff --git a/src/core/database/database.service.ts b/src/core/database/database.service.ts index d9adeff..27002b5 100644 --- a/src/core/database/database.service.ts +++ b/src/core/database/database.service.ts @@ -35,11 +35,9 @@ export class DatabaseService implements MongooseOptionsFactory { * @returns {MongooseModuleOptions} The Mongoose module options. */ createMongooseOptions(): MongooseModuleOptions { - this.logger.error('Creating Mongoose options...'); const autoPopulate: boolean = this.configService.get('database.autoPopulate', { infer: true, }); - this.logger.error(`Auto-populate: ${autoPopulate}`); return { uri: this.configService.get('database.uri', { infer: true, diff --git a/src/main.ts b/src/main.ts index 157283a..4f723d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,6 +23,9 @@ 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; + /** * Bootstrap the NestJS application. * @@ -159,6 +162,15 @@ export async function bootstrap(): Promise { } }); + // 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()); + } + } + // Return the application instance return app; } diff --git a/src/metadata.ts b/src/metadata.ts index 4a101f3..eaa6471 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -3,8 +3,9 @@ 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("./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/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 } } }], [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 } } }]] } }; + return { "@nestjs/swagger": { "models": [[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.service.ts b/src/modules/auth/auth.service.ts index 504f22e..42b3016 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -5,50 +5,64 @@ import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcryptjs'; -import { generateOTPCode } from 'src/utils'; +import { generateOTPCode, generateSlug } from 'src/utils'; import { BadRequestException, UnauthorizedException } from 'src/core/exceptions'; 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'; import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; import { JwtUserPayload } from './interfaces'; @Injectable() export class AuthService { + private saltOrRounds: number; + constructor( private readonly configService: ConfigService, private readonly jwtService: JwtService, private readonly userQueryService: UserQueryService, private readonly workspaceQueryService: WorkspaceQueryService, - ) {} + ) { + 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.configService.get('authentication.bcryptSaltRounds', { - infer: true, - }); - const hashedPassword = await bcrypt.hash(password, saltOrRounds); + const hashedPassword: string = await bcrypt.hash(password, this.saltOrRounds); const userPayload: User = { email, password: hashedPassword, workspace: workspace, name, - verified: true, + verified: false, registerCode: generateOTPCode(), verificationCode: null, verificationCodeExpiry: null, @@ -65,27 +79,31 @@ export class AuthService { 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/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/user/user.controller.ts b/src/modules/user/user.controller.ts index b94798f..81fbc4d 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,13 +1,14 @@ // 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'; import { ApiErrorResponses } from 'src/shared'; import { GetUser } from '../auth/decorators'; import { GetProfileResDto } from './dtos'; -import { UserDocument } from './user.schema'; +import { UserQueryService } from './user.query.service'; +import { User, UserDocument } from './user.schema'; /** * Controller for handling user-related operations. @@ -32,8 +33,10 @@ import { UserDocument } from './user.schema'; 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, }) @@ -45,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.query.service.ts b/src/modules/user/user.query.service.ts index 0475d04..4b79961 100644 --- a/src/modules/user/user.query.service.ts +++ b/src/modules/user/user.query.service.ts @@ -14,6 +14,19 @@ import { User, UserDocument } from './user.schema'; export class UserQueryService { constructor(private readonly userRepository: UserRepository) {} + /** + * 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. 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..e3843dc --- /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 } from 'src/shared'; + +import { CreateWorkspaceReqDto, FindWorkspaceBySlugReqDto } from './dtos'; +import { WorkspaceQueryService } from './workspace.query-service'; +import { Workspace } from './workspace.schema'; + +@ApiBearerAuth() +@ApiErrorResponses() +@ApiTags('Workspaces') +@Controller('workspaces') +export class WorkspaceController { + constructor(private readonly workspaceQueryService: WorkspaceQueryService) {} + + @HttpCode(HttpStatus.CREATED) + @ApiCreatedResponse({ + type: Workspace, + }) + @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) + @ApiOkResponse({ + type: Workspace, + isArray: true, + }) + @Get() + async findAll(): Promise { + return await this.workspaceQueryService.findAllWorkspaces(); + } + + @HttpCode(HttpStatus.OK) + @ApiOkResponse({ + type: Workspace, + }) + @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 737245d..d59c690 100644 --- a/src/modules/workspace/workspace.module.ts +++ b/src/modules/workspace/workspace.module.ts @@ -3,6 +3,7 @@ import { MongooseModule } from '@nestjs/mongoose'; 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'; @@ -20,6 +21,9 @@ import { WorkspaceSchema } from './workspace.schema'; * @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. @@ -29,6 +33,7 @@ import { WorkspaceSchema } from './workspace.schema'; */ @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 719f5b2..96ded4f 100644 --- a/src/modules/workspace/workspace.query-service.ts +++ b/src/modules/workspace/workspace.query-service.ts @@ -32,6 +32,21 @@ 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. * @@ -47,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 089d99b..c065b75 100644 --- a/src/modules/workspace/workspace.repository.ts +++ b/src/modules/workspace/workspace.repository.ts @@ -60,6 +60,16 @@ export class WorkspaceRepository extends DatabaseAbstractRepository { + return this.findOne({ slug }); + } + /** * Finds a single Workspace document based on the provided filter and updates it. * diff --git a/src/modules/workspace/workspace.schema.ts b/src/modules/workspace/workspace.schema.ts index 487ee2d..52db074 100644 --- a/src/modules/workspace/workspace.schema.ts +++ b/src/modules/workspace/workspace.schema.ts @@ -24,6 +24,19 @@ export type WorkspaceDocument = HydratedDocument; */ @Schema(getDatabaseSchemaOptions(DatabaseCollectionNames.WORKSPACE)) export class Workspace extends EntityDocumentHelper { + @ApiProperty({ + type: String, + description: 'The slug of the workspace', + example: 'my-workspace', + }) + @Prop({ + type: String, + required: true, + unique: true, + lowercase: true, + }) + slug: string; + @ApiProperty({ type: String, description: 'The name of the workspace', @@ -32,6 +45,7 @@ export class Workspace extends EntityDocumentHelper { @Prop({ type: String, required: true, + uppercase: true, }) name: string; diff --git a/src/utils/index.ts b/src/utils/index.ts index 288a56d..e82d24f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,5 +2,6 @@ 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/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, '-'); +}; From 7a211260c256c654b578620698713a5a863adc12 Mon Sep 17 00:00:00 2001 From: Alexander Daza Date: Thu, 10 Oct 2024 05:49:15 -0500 Subject: [PATCH 9/9] chore: base refactor --- .swcrc | 13 ++ nest-cli.json | 7 +- .../constants/exceptions.constants.ts | 89 +++++----- src/core/exceptions/index.ts | 1 + .../interfaces/exceptions.interface.ts | 29 +++- src/core/exceptions/not-found.exception.ts | 156 ++++++++++++++++++ src/core/filters/index.ts | 1 + .../filters/not-found-exception.filter.ts | 34 ++-- .../unprocessable-entity-exception.filter.ts | 37 +++++ src/core/interceptors/index.ts | 1 + src/core/interceptors/response.interceptor.ts | 24 +++ src/main.ts | 4 + src/metadata.ts | 2 +- src/modules/workspace/workspace.controller.ts | 16 +- .../api-global-response.decorator.ts | 89 ++++++++++ src/shared/decorators/index.ts | 1 + src/shared/dtos/index.ts | 1 + src/shared/dtos/response.dto.ts | 11 ++ src/shared/index.ts | 1 + 19 files changed, 449 insertions(+), 68 deletions(-) create mode 100644 .swcrc create mode 100644 src/core/exceptions/not-found.exception.ts create mode 100644 src/core/filters/unprocessable-entity-exception.filter.ts create mode 100644 src/core/interceptors/response.interceptor.ts create mode 100644 src/shared/decorators/api-global-response.decorator.ts create mode 100644 src/shared/dtos/index.ts create mode 100644 src/shared/dtos/response.dto.ts 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/nest-cli.json b/nest-cli.json index fedc9f3..666fb58 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -4,7 +4,12 @@ "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, - "builder": "swc", + "builder": { + "type": "swc", + "options": { + "swcrcPath": ".swcrc" + } + }, "typeCheck": true, "plugins": [ { diff --git a/src/core/exceptions/constants/exceptions.constants.ts b/src/core/exceptions/constants/exceptions.constants.ts index 11eedb6..85fcce6 100644 --- a/src/core/exceptions/constants/exceptions.constants.ts +++ b/src/core/exceptions/constants/exceptions.constants.ts @@ -3,6 +3,12 @@ * * @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. @@ -50,65 +56,72 @@ * @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: 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 + 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: 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 + 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: 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 + 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: 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 + 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/index.ts b/src/core/exceptions/index.ts index 853406b..e1236c3 100644 --- a/src/core/exceptions/index.ts +++ b/src/core/exceptions/index.ts @@ -5,4 +5,5 @@ 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 index 2b4817a..5face96 100644 --- a/src/core/exceptions/interfaces/exceptions.interface.ts +++ b/src/core/exceptions/interfaces/exceptions.interface.ts @@ -15,6 +15,27 @@ export interface IException { 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. * @@ -37,7 +58,7 @@ export interface IHttpBadRequestExceptionResponse { /** * Interface representing the structure of a not found exception response. * - * @interface IHttpNotFoundExceptionResponse + * @interface IHttpInternalServerErrorExceptionResponse * * @property {number} code - The HTTP status code of the not found error. * @property {string} message - A brief message describing the error. @@ -94,13 +115,14 @@ export interface IHttpForbiddenExceptionResponse { /** * Interface representing the structure of a conflict exception response. * - * @interface IHttpConflictExceptionResponse + * @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; @@ -114,11 +136,12 @@ export interface IHttpGatewayTimeOutExceptionResponse { /** * Interface representing the structure of a request timeout exception response. * - * @interface IHttpRequestTimeoutExceptionResponse + * @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. */ 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/core/filters/index.ts b/src/core/filters/index.ts index fa65da9..c908c00 100644 --- a/src/core/filters/index.ts +++ b/src/core/filters/index.ts @@ -5,4 +5,5 @@ 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/core/filters/not-found-exception.filter.ts b/src/core/filters/not-found-exception.filter.ts index 2cd0cc8..f28055f 100644 --- a/src/core/filters/not-found-exception.filter.ts +++ b/src/core/filters/not-found-exception.filter.ts @@ -1,17 +1,14 @@ -import { ArgumentsHost, Catch, ExceptionFilter, 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 - * @type {${1:*}} - */ - private readonly logger = new Logger(NotFoundExceptionFilter.name); + private readonly logger: Logger = new Logger(NotFoundExceptionFilter.name); /** * Creates an instance of `NotFoundExceptionFilter`. @@ -29,27 +26,30 @@ export class NotFoundExceptionFilter implements ExceptionFilter { */ 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.getStatus(); - const instanceException: NotFoundException = exception instanceof NotFoundException ? exception : new NotFoundException(exception); + const request = ctx.getRequest(); - const httpStatus = exception instanceof NotFoundException ? exception.getStatus() : HttpStatus.NOT_FOUND; + 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}`); + } - const request = ctx.getRequest(); + // Sets the trace ID from the request object to the exception. + exception.setTraceId(request.id); // Construct the response body. - const responseBody: Record = { - code: httpStatus, - message: 'Not Found', - description: instanceException.message, - timestamp: new Date().toISOString(), - traceId: request.id, - }; + const responseBody: IHttpNotFoundExceptionResponse = exception.generateHttpResponseBody(); // Send the HTTP response. httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 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/core/interceptors/index.ts b/src/core/interceptors/index.ts index 2c59527..9e62c4a 100644 --- a/src/core/interceptors/index.ts +++ b/src/core/interceptors/index.ts @@ -1,2 +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/main.ts b/src/main.ts index 4f723d1..2893dc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ 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: Logger = new Logger('Application Bootstrap'); @@ -122,6 +123,9 @@ export async function bootstrap(): Promise { }); } + // Set up the global HTTP response + app.useGlobalInterceptors(new HttpResponseInterceptor()); + // Set up the validation pipe app.useGlobalPipes(new ValidationPipe(validationOptionsUtil)); diff --git a/src/metadata.ts b/src/metadata.ts index eaa6471..01c38be 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -7,5 +7,5 @@ export default async () => { ["./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("./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 } } }]] } }; + 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/workspace/workspace.controller.ts b/src/modules/workspace/workspace.controller.ts index e3843dc..b64718b 100644 --- a/src/modules/workspace/workspace.controller.ts +++ b/src/modules/workspace/workspace.controller.ts @@ -2,22 +2,22 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post } from '@nestj import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { generateSlug } from 'src/utils'; -import { ApiErrorResponses } from 'src/shared'; +import { ApiErrorResponses, ApiGlobalResponse } from 'src/shared'; import { CreateWorkspaceReqDto, FindWorkspaceBySlugReqDto } from './dtos'; import { WorkspaceQueryService } from './workspace.query-service'; import { Workspace } from './workspace.schema'; @ApiBearerAuth() -@ApiErrorResponses() @ApiTags('Workspaces') @Controller('workspaces') export class WorkspaceController { constructor(private readonly workspaceQueryService: WorkspaceQueryService) {} @HttpCode(HttpStatus.CREATED) - @ApiCreatedResponse({ - type: Workspace, + @ApiGlobalResponse(Workspace, { + description: 'The workspace has been successfully created.', + isCreatedResponse: true, }) @Post() async create(@Body() createWorkspaceReqDto: CreateWorkspaceReqDto): Promise { @@ -38,8 +38,8 @@ export class WorkspaceController { } @HttpCode(HttpStatus.OK) - @ApiOkResponse({ - type: Workspace, + @ApiGlobalResponse(Workspace, { + description: 'The workspaces have been successfully retrieved.', isArray: true, }) @Get() @@ -48,8 +48,8 @@ export class WorkspaceController { } @HttpCode(HttpStatus.OK) - @ApiOkResponse({ - type: Workspace, + @ApiGlobalResponse(Workspace, { + description: 'The workspace has been successfully retrieved.', }) @Get(':id') async findOneById(@Param('id') id: string): Promise { 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 index d16eb17..0cf2513 100644 --- a/src/shared/decorators/index.ts +++ b/src/shared/decorators/index.ts @@ -1 +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/index.ts b/src/shared/index.ts index 6310436..1571487 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,3 +1,4 @@ export * from './decorators'; +export * from './dtos'; export * from './enums'; export * from './types';