├── .dockerignore ├── .env.sample ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile-dev ├── Dockerfile-prod ├── LICENSE ├── README.md ├── docker-compose.yml ├── eslint.config.mjs ├── nodemon-js.json ├── nodemon-ts.json ├── nodemon.json ├── package.json ├── src-javascript ├── app │ ├── apis │ │ └── user │ │ │ ├── controllers │ │ │ ├── login.user.controller.js │ │ │ └── register.user.controller.js │ │ │ ├── repositories │ │ │ ├── mongoose.user.repository.js │ │ │ └── sequelize.user.repository.js │ │ │ └── services │ │ │ └── user.service.js │ ├── common │ │ └── interfaces.js │ ├── crons │ │ └── demo.cron.js │ ├── enums │ │ ├── CronJob.js │ │ └── StatusCodes.js │ ├── handlers │ │ ├── CustomErrorHandler.js │ │ └── JoiErrorHandler.js │ ├── models │ │ ├── mongoose.user.model.js │ │ └── sequelize.user.model.js │ ├── routes │ │ └── user.routes.js │ └── utils │ │ ├── AsyncHandler.js │ │ ├── CronBuilder.js │ │ ├── EncryptionUtil.js │ │ ├── MasterController.js │ │ ├── RequestBuilder.js │ │ └── ResponseBuilder.js ├── config │ ├── cronConfig.js │ ├── expressConfig.js │ ├── mongooseConfig.js │ ├── sequelizeConfig.js │ ├── socketConfig.js │ └── swaggerConfig.js └── server.js ├── src-typescript ├── app │ ├── apis │ │ └── user │ │ │ ├── controllers │ │ │ ├── login.user.controller.ts │ │ │ └── register.user.controller.ts │ │ │ ├── interfaces.ts │ │ │ ├── repositories │ │ │ ├── mongoose.user.repository.ts │ │ │ └── sequelize.user.repository.ts │ │ │ └── services │ │ │ └── user.service.ts │ ├── common │ │ └── interfaces.ts │ ├── crons │ │ └── demo.cron.ts │ ├── enums │ │ ├── CronJob.ts │ │ └── StatusCodes.ts │ ├── handlers │ │ ├── CustomErrorHandler.ts │ │ └── JoiErrorHandler.ts │ ├── models │ │ ├── mongoose.user.model.ts │ │ └── sequelize.user.model.ts │ ├── routes │ │ └── user.routes.ts │ └── utils │ │ ├── AsyncHandler.ts │ │ ├── CronBuilder.ts │ │ ├── EncryptionUtil.ts │ │ ├── MasterController.ts │ │ ├── RequestBuilder.ts │ │ └── ResponseBuilder.ts ├── config │ ├── cronConfig.ts │ ├── expressConfig.ts │ ├── mongooseConfig.ts │ ├── sequelizeConfig.ts │ ├── socketConfig.ts │ └── swaggerConfig.ts └── server.ts ├── swagger.json ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .gitignore 4 | .prettierignore 5 | .prettierrc 6 | Dockerfile 7 | LICENSE 8 | README.md -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | 3 | # choose between postgres, mysql, mongodb, sqlite, mssql, db2, snowflake, oracle 4 | DB_DIALECT=mongodb 5 | 6 | # for postgres, mysql, mariadb 7 | DB_HOST=localhost 8 | DB_PORT=5432 9 | DB_NAME=postgres 10 | DB_USER=postgres 11 | DB_PASS=password 12 | 13 | # for mongodb 14 | MONGO_URI=mongodb://127.0.0.1:27017/Test 15 | 16 | JWT_SECRET=secret -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Other Repos 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy1: 10 | name: Deploy to express-master-controller 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Fire Deploy Event 14 | run: | 15 | curl --location 'https://api.github.com/repos/thre4dripper/express-master-controller/actions/workflows/96432150/dispatches' \ 16 | --header 'Accept: application/vnd.github+json' \ 17 | --header 'Authorization: Bearer ${{ secrets.WORKFLOW_TOKEN }}' \ 18 | --header 'Content-Type: application/json' \ 19 | --data '{ "ref": "main" }' 20 | 21 | deploy2: 22 | name: Deploy to node-server-init 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Fire Deploy Event 26 | run: | 27 | curl --location 'https://api.github.com/repos/thre4dripper/node-server-init/actions/workflows/96324202/dispatches' \ 28 | --header 'Accept: application/vnd.github+json' \ 29 | --header 'Authorization: Bearer ${{ secrets.WORKFLOW_TOKEN }}' \ 30 | --header 'Content-Type: application/json' \ 31 | --data '{ "ref": "main" }' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /dist 4 | yarn-error.log 5 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | .env 4 | .env.sample 5 | .gitignore 6 | yarn.lock 7 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "endOfLine": "auto", 7 | "printWidth": 100, 8 | "bracketSpacing": true, 9 | "useTabs": false, 10 | "bracketSameLine": true, 11 | "arrowParens": "always" 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | # Use -f flag to specify the Dockerfile when building the image manually 2 | # Use the official Node.js v22 image 3 | FROM node:22 4 | 5 | # Create app directory 6 | WORKDIR /app 7 | 8 | # Install nodemon and ts-node globally 9 | RUN npm install -g nodemon ts-node 10 | 11 | # Copy package.json and yarn.lock files 12 | COPY package*.json ./ 13 | COPY yarn.lock ./ 14 | 15 | # Install app dependencies 16 | RUN yarn 17 | 18 | # Copy source files 19 | COPY . . -------------------------------------------------------------------------------- /Dockerfile-prod: -------------------------------------------------------------------------------- 1 | # Use -f flag to specify the Dockerfile when building the image manually 2 | # Use the official Node.js v22 image 3 | FROM node:22 as build 4 | 5 | # Create app directory 6 | WORKDIR /app 7 | 8 | # Copy package.json and yarn.lock files 9 | COPY package*.json ./ 10 | COPY yarn.lock ./ 11 | 12 | # Install app dependencies 13 | RUN yarn 14 | 15 | # Copy source files 16 | COPY . . 17 | 18 | # Build the application 19 | RUN yarn build 20 | 21 | # NEXT STAGE: Create a new image with only the built files 22 | FROM node:22-alpine 23 | 24 | # Create app directory 25 | WORKDIR /app 26 | 27 | # Copy built files from the previous stage 28 | COPY --from=build /app/dist ./dist 29 | 30 | # Copy package.json and yarn.lock files 31 | COPY --from=build /app/package*.json ./ 32 | COPY --from=build /app/yarn.lock ./ 33 | 34 | # Copy swagger files 35 | COPY --from=build /app/swagger.json ./ 36 | 37 | # Install app dependencies 38 | RUN yarn --production 39 | 40 | # Expose the port the app runs on 41 | EXPOSE 3000 42 | 43 | # Define the command to run the application 44 | CMD ["yarn", "preview"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ijlal Ahmad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeTs-Express-Service-Based-Template 2 | 3 | ![Node](https://img.shields.io/badge/-Node-339933?style=flat-square&logo=Node.js&logoColor=white) 4 | ![TypeScript](https://img.shields.io/badge/-TypeScript-007ACC?style=flat-square&logo=TypeScript&logoColor=white) 5 | ![Express](https://img.shields.io/badge/-Express-000000?style=flat-square&logo=Express&logoColor=white) 6 | ![Sequelize](https://img.shields.io/badge/-Sequelize-52B0E7?style=flat-square&logo=Sequelize&logoColor=white) 7 | ![MySQL](https://img.shields.io/badge/-MySQL-4479A1?style=flat-square&logo=MySQL&logoColor=white) 8 | ![PostgresSQL](https://img.shields.io/badge/-PostgreSQL-336791?style=flat-square&logo=PostgreSQL&logoColor=white) 9 | ![Sqlite](https://img.shields.io/badge/-Sqlite-003B57?style=flat-square&logo=Sqlite&logoColor=white) 10 | ![MariaDB](https://img.shields.io/badge/-MariaDB-003545?style=flat-square&logo=MariaDB&logoColor=white) 11 | ![MSSql](https://img.shields.io/badge/-MSSql-CC2927?style=flat-square&logo=Microsoft-SQL-Server&logoColor=white) 12 | ![DB2](https://img.shields.io/badge/-DB2-CC0000?style=flat-square&logo=IBM&logoColor=white) 13 | ![Snowflake](https://img.shields.io/badge/-Snowflake-00BFFF?style=flat-square&logo=Snowflake&logoColor=white) 14 | ![Oracle](https://img.shields.io/badge/-Oracle-F80000?style=flat-square&logo=Oracle&logoColor=white) 15 | ![Mongoose](https://img.shields.io/badge/-Mongoose-880000?style=flat-square&logo=Mongoose&logoColor=white) 16 | ![MongoDB](https://img.shields.io/badge/-MongoDB-47A248?style=flat-square&logo=MongoDB&logoColor=white) 17 | ![Validations](https://img.shields.io/badge/-Validations-FF0000?style=flat-square) 18 | ![Socket](https://img.shields.io/badge/-Socket-FF6900?style=flat-square&logo=Socket.io&logoColor=white) 19 | ![Docker](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&logo=Docker&logoColor=white) 20 | ![Swagger](https://img.shields.io/badge/-Swagger-85EA2D?style=flat-square&logo=Swagger&logoColor=white) 21 | ![CronJobs](https://img.shields.io/badge/-CronJobs-00FFFF?style=flat-square) 22 | 23 | A fully configurable Node.js, Express, and TypeScript server template with a service-based architecture 24 | that can interact with MySQL, PostgresSQL, Sqlite, MariaDB, MSSql, DB2, Snowflake, Oracle, MongoDB. 25 | Out-of-the-box validation, documentation generation, and 26 | more. 27 | 28 | ## Features 29 | 30 | - **Node.js, Express, TypeScript**: Robust server setup using Node.js, Express, and TypeScript. 31 | - **Sequelize**: Integration with Sequelize for SQL database operations. 32 | - **Mongoose**: Integration with Mongoose for MongoDB database operations. 33 | - **Database Compatibility**: Interact with MySQL, PostgreSQL, MariaDB, Sqlite, MSSql, MongoDB. 34 | - **Validation Mechanism**: Pre-built validations for request payloads. 35 | - **Automated Swagger Documentation**: Automatically generated documentation available at `/api-docs`. 36 | - **Service-Based Architecture**: Modular approach for better organization and scalability. 37 | - **Socket Events**: Socket event handling using Socket.io. 38 | - **Docker**: Dockerized for easy deployment. 39 | - **Cron Jobs**: Schedule tasks using cron jobs. 40 | 41 | ## Modules 42 | 43 | ### Automated Swagger Docs 44 | 45 | - Swagger documentation auto-generated for all routes. 46 | - Accessible at `/api-docs`. 47 | - Generated using the `doc` method in the `MasterController` class and Joi validation schemas. 48 | 49 | ### MasterController (Heart of the application) 50 | 51 | The `MasterController` is the backbone, providing functionalities for managing HTTP requests, socket events, payload 52 | validation, and more. 53 | 54 | #### Features 55 | 56 | - **Controller Logic Handling**: `restController` method manages HTTP requests. 57 | - **Socket Event Handling**: `socketController` method manages socket events. 58 | - **Cron Job Scheduling**: `cronController` method schedules cron jobs. 59 | - **Payload Validation**: `joiValidator` method validates incoming request payloads. 60 | - **Swagger Documentation Generation**: `doc` method generates Swagger documentation. 61 | - **Route Handling**: `get`, `post`, `put`, and `delete` methods register routes within the Express router. 62 | 63 | #### Usage 64 | 65 | Extend the `MasterController` to create controller classes for specific routes or socket events. Use the provided 66 | methods for efficient request handling, validation, and documentation generation. 67 | 68 | ## Installation 69 | 70 | ### Automated (CLI Tool) 71 | 72 | > Easily initialize a Node.js server project with custom configurations using our CLI tool: 73 | > 74 | > ```bash 75 | > npx node-server-init 76 | > ``` 77 | > 78 | > If you want to use the current directory as the project folder, run the following command: 79 | > 80 | > ```bash 81 | > npx node-server-init . 82 | > ``` 83 | > 84 | > For more information, visit the [CLI Tool](https://www.npmjs.com/package/node-server-init) page or 85 | > the [GitHub](https://github.com/Thre4dripper/node-server-init) repository. 86 | 87 | ### Manual 88 | 89 | > #### Clone this repo to your local machine using ` 90 | > 91 | > ```bash 92 | > $ git clone https://github.com/Thre4dripper/NodeTs-Express-Service-Based-Template 93 | > ``` 94 | > #### Install dependencies 95 | > 96 | > ```bash 97 | > $ npm install or yarn 98 | > ``` 99 | > 100 | > #### Start the server 101 | > 102 | > ```bash 103 | > $ npm run dev or yarn dev 104 | > ``` 105 | > 106 | > #### Build the project 107 | > 108 | > ```bash 109 | > $ npm run build or yarn build 110 | > ``` 111 | > 112 | > #### Run the project 113 | > 114 | > ```bash 115 | > $ npm run start or yarn start 116 | > ``` 117 | 118 | ### Database Setup 119 | 120 | > Additional dependencies are required for database operations. Install the appropriate dependencies for your database 121 | > dialect. 122 | > 123 | > #### MySQL 124 | > 125 | > ```bash 126 | > $ npm install mysql2 127 | > ``` 128 | > 129 | > #### PostgreSQL 130 | > 131 | > ```bash 132 | > $ npm install pg pg-hstore 133 | > ``` 134 | > 135 | > #### Sqlite 136 | > 137 | > ```bash 138 | > $ npm install sqlite 139 | > ``` 140 | > 141 | > #### MariaDB 142 | > 143 | > ```bash 144 | > $ npm install mariadb 145 | > ``` 146 | > 147 | > #### MSSql 148 | > 149 | > ```bash 150 | > $ npm install tedious or mssql 151 | > ``` 152 | > 153 | > #### DB2 154 | > 155 | > ```bash 156 | > $ npm install ibm_db 157 | > ``` 158 | > 159 | > #### Snowflake 160 | > 161 | > ```bash 162 | > $ npm install snowflake-sdk 163 | > ``` 164 | > 165 | > #### Oracle 166 | > 167 | > ```bash 168 | > $ npm install oracledb 169 | > ``` 170 | > 171 | > #### MongoDB 172 | > 173 | > ```bash 174 | > $ npm install mongoose 175 | > ``` 176 | 177 | ## Creating APIs 178 | 179 | ### Controller 180 | 181 | ```typescript 182 | class Controller extends MasterController { 183 | // swagger documetation for the api 184 | static doc() { 185 | return { 186 | tags: ['User'], 187 | summary: 'Register User', 188 | description: 'Register User', 189 | }; 190 | } 191 | 192 | // add your validations here, 193 | // rest of the swagger documentation will be generated automatically from the validation 194 | public static validate(): RequestBuilder { 195 | const payload = new RequestBuilder(); 196 | 197 | // request body validation 198 | payload.addToBody( 199 | Joi.object().keys({ 200 | name: Joi.string().required(), 201 | lastName: Joi.string().required(), 202 | email: Joi.string().email().required(), 203 | password: Joi.string().min(8).max(20).required(), 204 | }), 205 | ); 206 | 207 | // request query validation 208 | payload.addToQuery( 209 | Joi.object().keys({ 210 | limit: Joi.number().required(), 211 | offset: Joi.number().required(), 212 | }), 213 | ); 214 | 215 | // request params validation 216 | payload.addToParams( 217 | Joi.object().keys({ 218 | id: Joi.number().required(), 219 | }), 220 | ); 221 | return payload; 222 | } 223 | 224 | // controller function 225 | async restController( 226 | params: IParams, 227 | query: IQuery, 228 | body: IBody, 229 | headers: any, 230 | allData: any): Promise { 231 | // your code here 232 | return new ResponseBuilder(200, Response, 'Success Message'); 233 | } 234 | 235 | // socket controller function 236 | socketController(io: Server, socket: Socket, payload: any): void { 237 | // your code here 238 | // Socket data will be available in payload, recieved from the client on socket event, which is setup in the route file 239 | // You can emit data back to the client using io.emit or socket.emit 240 | } 241 | 242 | // cron controller function 243 | cronController(): void { 244 | // your scheduled code here (if any) 245 | } 246 | } 247 | 248 | export default Controller; 249 | ``` 250 | 251 | #### Controller Generics 252 | 253 | - **IParams:** Request params interface/type 254 | - **IQuery:** Request query interface/type 255 | - **IBody:** Request body interface/type 256 | 257 | #### restController Parameters 258 | 259 | - **params:** Request params (eg. /user/:id) 260 | - **query:** Request query (eg. /user?limit=10&offset=0) 261 | - **body:** Request body 262 | - **headers:** Request headers 263 | - **allData:** All request data (all the above-combined + custom data from middlewares) 264 | 265 | #### socketController Parameters 266 | 267 | - **io:** Socket.io instance 268 | - **socket:** Socket instance 269 | - **payload:** Data sent from the client 270 | 271 | ### Router File 272 | 273 | ```typescript 274 | import express from 'express' 275 | import Controller from '../Controller' 276 | 277 | export default (app: express.Application) => { 278 | // REST Routes 279 | Controller.get(app, '/user/:id', [ 280 | /* Comma separated middlewares */ 281 | ]) 282 | Controller.post(app, '/user/:id', [ 283 | /* Comma separated middlewares */ 284 | ]) 285 | Controller.put(app, '/user/:id', [ 286 | /* Comma separated middlewares */ 287 | ]) 288 | Controller.delete(app, '/user/:id', [ 289 | /* Comma separated middlewares */ 290 | ]) 291 | Controller.patch(app, '/user/:id', [ 292 | /* Comma separated middlewares */ 293 | ]) 294 | 295 | // Socket Events 296 | // Any payload you send from the client to this event will be available in the socketController function 297 | Controller.socketIO('Event Name') 298 | } 299 | ``` 300 | 301 | > **Important**: Make sure to name your router file as `*.routes.ts` or `*.routes.js` 302 | 303 | > **Note:** You don't need to import your router file to anywhere, 304 | > put it in the routes directory, and it will be automatically 305 | > taken care by the package. 306 | 307 | ### Cron File 308 | 309 | ```typescript 310 | class DemoCron extends MasterController { 311 | cronController() { 312 | console.log('Cron job is running'); 313 | } 314 | } 315 | 316 | // Unix Crontab format 317 | DemoCron.cronJob('*/5 * * * * *'); 318 | 319 | // Using CronBuilder 320 | DemoCron.cronJob( 321 | new CronBuilder() 322 | .every() 323 | .second() 324 | .every() 325 | .specificMinute([10, 20, 30]) 326 | .every() 327 | .dayOfMonth(CronMonth.January) 328 | .every() 329 | .dayOfWeek(CronWeekday.Friday) 330 | .build(), 331 | ); 332 | ``` 333 | 334 | > **Important**: Make sure to name your cron file as `*.cron.ts` or `*.cron.js` 335 | 336 | > **Note:** You don't need to import your cron file to anywhere, 337 | > put it in cron directory, and it will be automatically 338 | > taken care by the package. 339 | 340 | ## Docker 341 | 342 | > #### Docker Environment variables 343 | > 344 | > - `PORT` - Port number for the server to run on. 345 | > - `DB_DIALECT` - Database dialect to use. (Options: mysql, postgres, mariadb, sqlite, mssql, mongodb) 346 | > - `DB_HOST` - Database host. 347 | > - `DB_PORT` - Database port. 348 | > - `DB_USER` - Database username. 349 | > - `DB_PASS` - Database password. 350 | > - `DB_NAME` - Database name. 351 | > - `MONGO_URI` - MongoDB URI (Only for MongoDB Dialect). 352 | > - `JWT_SECRET` - Secret key for JWT. 353 | > 354 | > #### Build the image 355 | > 356 | > ```bash 357 | > $ docker build -t . 358 | > ``` 359 | > 360 | > #### Run the container 361 | > 362 | > ```bash 363 | > $ docker run -e = -p : 364 | > ``` 365 | > 366 | > #### Run the container in the background 367 | > 368 | > ```bash 369 | > $ docker run -d -e = -p : 370 | > ``` 371 | > 372 | > #### Stop the container 373 | > 374 | > ```bash 375 | > $ docker stop 376 | > ``` 377 | > 378 | > #### Remove the container 379 | > 380 | > ```bash 381 | > $ docker rm 382 | > ``` 383 | > 384 | > #### Remove the image 385 | > 386 | > ```bash 387 | > $ docker rmi 388 | > ``` 389 | 390 | ### Contributing 391 | 392 | Contributions are welcome! Please feel free to submit a Pull Request. 393 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres-local: 3 | image: postgres 4 | container_name: postgres 5 | restart: unless-stopped 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=password 9 | - POSTGRES_DB=postgres 10 | ports: 11 | - '5432:5432' 12 | volumes: 13 | - postgres-data:/var/lib/postgresql/data 14 | mongodb-local: 15 | image: mongo 16 | container_name: mongo 17 | restart: unless-stopped 18 | ports: 19 | - '27017:27017' 20 | volumes: 21 | - mongodb-data:/data/db 22 | app: 23 | build: 24 | context: . 25 | dockerfile: Dockerfile-dev 26 | # Remove command and develop keys when using Docker Compose in production 27 | command: sh -c "yarn build && yarn lint && node --harmony dist/server.js" 28 | develop: 29 | watch: 30 | - action: sync+restart 31 | path: ./src 32 | target: /app/src 33 | ignore: 34 | - node_modules/ 35 | - action: rebuild 36 | path: package.json 37 | image: node-app 38 | container_name: node-app 39 | restart: unless-stopped 40 | ports: 41 | - '3000:3000' 42 | # Remove environment key when using .env file 43 | # environment: 44 | # - PORT=3000 45 | # - DB_DIALECT=postgres 46 | # - DB_HOST=postgres-local 47 | # - DB_PORT=5432 48 | # - DB_NAME=postgres 49 | # - DB_USER=postgres 50 | # - DB_PASS=password 51 | # - JWT_SECRET=secret 52 | env_file: 53 | - .env 54 | depends_on: 55 | - postgres-local 56 | volumes: 57 | postgres-data: 58 | mongodb-data: 59 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import prettier from 'eslint-plugin-prettier'; 3 | import globals from 'globals'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import path from 'node:path'; 6 | import { fileURLToPath } from 'node:url'; 7 | import js from '@eslint/js'; 8 | import { FlatCompat } from '@eslint/eslintrc'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all, 16 | }); 17 | 18 | export default [ 19 | { 20 | files: ['**/*.ts', '**/*.js'], 21 | }, 22 | { 23 | ignores: [ 24 | '**/node_modules', 25 | '**/dist', 26 | '**/*.yml', 27 | '**/*.json', 28 | '**/*.md', 29 | '**/Dockerfile', 30 | 'docs/**/*.*', 31 | ], 32 | }, 33 | ...compat.extends('prettier', 'plugin:prettier/recommended'), 34 | { 35 | plugins: { 36 | '@typescript-eslint': typescriptEslint, 37 | prettier, 38 | }, 39 | 40 | languageOptions: { 41 | globals: { 42 | ...globals.node, 43 | }, 44 | 45 | parser: tsParser, 46 | ecmaVersion: 6, 47 | sourceType: 'module', 48 | 49 | parserOptions: { 50 | ecmaFeatures: { 51 | modules: true, 52 | }, 53 | }, 54 | }, 55 | 56 | rules: {}, 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /nodemon-js.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./nodemon-js.json", 3 | "exec": "yarn lint && node src-javascript/server.js || exit 1" 4 | } -------------------------------------------------------------------------------- /nodemon-ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./nodemon-ts.json", 3 | "exec": "yarn build && yarn lint && node --harmony dist/server.js || exit 1" 4 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "/node_modules/**", 4 | "/.git/**", 5 | "/dist/**" 6 | ], 7 | "verbose": true, 8 | "ext": "ts,js,html,json,yml,css" 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-service-based-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev-ts": "yarn prettier-ts && nodemon src-typescript/server.ts --config nodemon-ts.json", 8 | "dev-js": "yarn prettier-js && nodemon src-javascript/server.js --config nodemon-js.json", 9 | "build": "tsc && copyfiles -u 1 \"src-typescript/**/*.{css,html,ico,png,svg}\" dist/src/", 10 | "start-ts": "ts-node src-typescript/server.ts", 11 | "start-js": "node src-javascript/server.js", 12 | "preview": "node dist/server.js", 13 | "prettier-ts": "prettier --write \"src-typescript/**/*.ts\"", 14 | "prettier-js": "prettier --write \"src-javascript/**/*.js\"", 15 | "lint": "eslint" 16 | }, 17 | "author": "Ijlal Ahmad", 18 | "license": "ISC", 19 | "dependencies": { 20 | "axios": "^1.6.2", 21 | "bcrypt": "^5.1.1", 22 | "cors": "^2.8.5", 23 | "cron": "^3.1.7", 24 | "dotenv": "^16.3.1", 25 | "express": "^4.18.2", 26 | "joi": "^17.11.0", 27 | "joi-to-swagger": "^6.2.0", 28 | "jsonwebtoken": "^9.0.2", 29 | "mongoose": "^8.0.3", 30 | "morgan": "^1.10.0", 31 | "pg": "^8.11.3", 32 | "pg-hstore": "^2.3.4", 33 | "process": "^0.11.10", 34 | "reflect-metadata": "^0.2.1", 35 | "sequelize": "^6.35.2", 36 | "sequelize-typescript": "^2.1.6", 37 | "socket.io": "^4.7.2", 38 | "swagger-ui-express": "^5.0.0" 39 | }, 40 | "devDependencies": { 41 | "@eslint/eslintrc": "^3.1.0", 42 | "@eslint/js": "^9.12.0", 43 | "@types/bcrypt": "^5.0.2", 44 | "@types/cors": "^2.8.17", 45 | "@types/express": "^4.17.21", 46 | "@types/jsonwebtoken": "^9.0.5", 47 | "@types/morgan": "^1.9.9", 48 | "@types/node": "^22.7.5", 49 | "@types/swagger-ui-express": "^4.1.6", 50 | "@typescript-eslint/eslint-plugin": "^8.8.1", 51 | "@typescript-eslint/parser": "^8.8.1", 52 | "copyfiles": "^2.4.1", 53 | "eslint": "9.12.0", 54 | "eslint-config-prettier": "^9.1.0", 55 | "eslint-plugin-prettier": "^5.1.3", 56 | "globals": "^15.11.0", 57 | "nodemon": "^3.0.2", 58 | "prettier": "^3.1.1", 59 | "ts-node": "^10.9.2", 60 | "typescript": "^5.4.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src-javascript/app/apis/user/controllers/login.user.controller.js: -------------------------------------------------------------------------------- 1 | const MasterController = require('../../../utils/MasterController'); 2 | const { default: RequestBuilder } = require('../../../utils/RequestBuilder'); 3 | const Joi = require('joi'); 4 | const userService = require('../services/user.service'); 5 | const ResponseBuilder = require('../../../utils/ResponseBuilder'); 6 | const { StatusCodes } = require('../../../enums/StatusCodes'); 7 | 8 | class LoginUserController extends MasterController { 9 | static doc() { 10 | return { 11 | tags: ['User'], 12 | summary: 'Login User', 13 | description: 'Login User', 14 | }; 15 | } 16 | 17 | static validate() { 18 | const payload = new RequestBuilder(); 19 | payload.addToBody( 20 | Joi.object().keys({ 21 | email: Joi.string().email().required(), 22 | password: Joi.string().min(8).max(20).required(), 23 | }) 24 | ); 25 | return payload; 26 | } 27 | 28 | async restController(params, query, body, headers, allData) { 29 | const { email, password } = body; 30 | const response = await userService.loginUser({ email, password }); 31 | return new ResponseBuilder(StatusCodes.SUCCESS, response, 'User logged in successfully'); 32 | } 33 | } 34 | 35 | module.exports = LoginUserController; 36 | -------------------------------------------------------------------------------- /src-javascript/app/apis/user/controllers/register.user.controller.js: -------------------------------------------------------------------------------- 1 | const MasterController = require('../../../utils/MasterController'); 2 | const { StatusCodes } = require('../../../enums/StatusCodes'); 3 | const ResponseBuilder = require('../../../utils/ResponseBuilder'); 4 | const { default: RequestBuilder } = require('../../../utils/RequestBuilder'); 5 | const Joi = require('joi'); 6 | const userService = require('../services/user.service'); 7 | 8 | class RegisterUserController extends MasterController { 9 | static doc() { 10 | return { 11 | tags: ['User'], 12 | summary: 'Register User', 13 | description: 'Register User', 14 | }; 15 | } 16 | 17 | static validate() { 18 | const payload = new RequestBuilder(); 19 | payload.addToBody( 20 | Joi.object().keys({ 21 | name: Joi.string().required(), 22 | email: Joi.string().email().required(), 23 | password: Joi.string().min(8).max(20).required(), 24 | }) 25 | ); 26 | return payload; 27 | } 28 | 29 | async restController(params, query, body, headers, allData) { 30 | const { name, email, password } = body; 31 | const response = await userService.registerUser({ name, email, password }); 32 | return new ResponseBuilder(StatusCodes.SUCCESS, response, 'User registered successfully'); 33 | } 34 | } 35 | 36 | module.exports = RegisterUserController; 37 | -------------------------------------------------------------------------------- /src-javascript/app/apis/user/repositories/mongoose.user.repository.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/mongoose.user.model'); 2 | 3 | class UserRepository { 4 | async find(filter) { 5 | return User.findOne(filter).exec(); 6 | } 7 | 8 | async create(data) { 9 | return User.create(data); 10 | } 11 | } 12 | 13 | module.exports = new UserRepository(); 14 | -------------------------------------------------------------------------------- /src-javascript/app/apis/user/repositories/sequelize.user.repository.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/sequelize.user.model'); 2 | 3 | class UserRepository { 4 | async find(filter) { 5 | return User.findOne({ 6 | where: filter, 7 | }); 8 | } 9 | 10 | async create(data) { 11 | return User.create(data); 12 | } 13 | } 14 | 15 | module.exports = new UserRepository(); 16 | -------------------------------------------------------------------------------- /src-javascript/app/apis/user/services/user.service.js: -------------------------------------------------------------------------------- 1 | const userRepository = require('../repositories/sequelize.user.repository'); 2 | const { ValidationError } = require('../../../handlers/CustomErrorHandler'); 3 | const EncryptionUtil = require('../../../utils/EncryptionUtil'); 4 | 5 | class UserService { 6 | async registerUser(data) { 7 | const user = await userRepository.find({ email: data.email }); 8 | if (user) { 9 | throw new ValidationError('User already exists'); 10 | } 11 | data.password = await EncryptionUtil.hashPassword(data.password); 12 | return userRepository.create(data); 13 | } 14 | 15 | async loginUser(data) { 16 | const user = await userRepository.find({ email: data.email }); 17 | if (!user) { 18 | throw new ValidationError('User does not exist'); 19 | } 20 | const isPasswordValid = await EncryptionUtil.comparePassword( 21 | data.password, 22 | user.password ?? '' 23 | ); 24 | if (!isPasswordValid) { 25 | throw new ValidationError('Invalid password'); 26 | } 27 | return user; 28 | } 29 | } 30 | 31 | module.exports = new UserService(); 32 | -------------------------------------------------------------------------------- /src-javascript/app/common/interfaces.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src-javascript/app/crons/demo.cron.js: -------------------------------------------------------------------------------- 1 | const MasterController = require('../utils/MasterController'); 2 | const CronBuilder = require('../utils/CronBuilder'); 3 | const { CronWeekday } = require('../enums/CronJob'); 4 | 5 | class DemoCron extends MasterController { 6 | cronController() { 7 | console.log('Cron job is running'); 8 | } 9 | } 10 | 11 | // Unix Crontab format 12 | DemoCron.cronJob('*/5 * * * * *'); 13 | 14 | // Using CronBuilder 15 | DemoCron.cronJob( 16 | new CronBuilder() 17 | .every() 18 | .second() 19 | .every() 20 | .specificMinute([10, 20, 30]) 21 | .every() 22 | .dayOfMonth() 23 | .every() 24 | .dayOfWeek(CronWeekday.Friday) 25 | .build() 26 | ); 27 | -------------------------------------------------------------------------------- /src-javascript/app/enums/CronJob.js: -------------------------------------------------------------------------------- 1 | const CronMonth = { 2 | January: 'jan', 3 | February: 'feb', 4 | March: 'mar', 5 | April: 'apr', 6 | May: 'may', 7 | June: 'jun', 8 | July: 'jul', 9 | August: 'aug', 10 | September: 'sep', 11 | October: 'oct', 12 | November: 'nov', 13 | December: 'dec', 14 | }; 15 | 16 | const CronWeekday = { 17 | Sunday: 'sun', 18 | Monday: 'mon', 19 | Tuesday: 'tue', 20 | Wednesday: 'wed', 21 | Thursday: 'thu', 22 | Friday: 'fri', 23 | Saturday: 'sat', 24 | }; 25 | 26 | module.exports.CronMonth = CronMonth; 27 | module.exports.CronWeekday = CronWeekday; 28 | -------------------------------------------------------------------------------- /src-javascript/app/enums/StatusCodes.js: -------------------------------------------------------------------------------- 1 | const StatusCodes = { 2 | SUCCESS: 200, 3 | CREATED: 201, 4 | UNAUTHORISED: 401, 5 | CONFLICT: 409, 6 | BAD_REQUEST: 400, 7 | SERVER_ERROR: 500, 8 | NOT_FOUND: 404, 9 | ALREADY_EXISTS: 403, 10 | }; 11 | 12 | module.exports.StatusCodes = StatusCodes; 13 | -------------------------------------------------------------------------------- /src-javascript/app/handlers/CustomErrorHandler.js: -------------------------------------------------------------------------------- 1 | const { STATUS_CODES } = require('node:http'); 2 | const { StatusCodes } = require('../enums/StatusCodes'); 3 | 4 | class ValidationError extends Error { 5 | constructor(message, errorCode) { 6 | super(message); 7 | this.name = 'ValidationError'; 8 | this.errorCode = errorCode; 9 | } 10 | } 11 | 12 | const customErrorHandler = (err, req, res, next) => { 13 | if (err.name === 'ValidationError') { 14 | // Handle ValidationError 15 | return res.status(err.errorCode || StatusCodes.BAD_REQUEST).json({ error: err.message }); // Respond with a 400 Bad Request and the error message 16 | } else { 17 | next(err); // Pass other errors to the default error handler 18 | } 19 | }; 20 | 21 | module.exports = customErrorHandler; 22 | exports.ValidationError = ValidationError; 23 | -------------------------------------------------------------------------------- /src-javascript/app/handlers/JoiErrorHandler.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const joiErrorHandler = (err, req, res, next) => { 4 | if (err instanceof Joi.ValidationError) { 5 | // Joi validation error occurred 6 | const errorMessage = err.details.map((error) => error.message).join(', '); 7 | res.status(400).json({ error: errorMessage }); // Respond with a 400 Bad Request and the error message 8 | } else { 9 | next(err); // Pass other errors to the default error handler 10 | } 11 | }; 12 | 13 | module.exports = joiErrorHandler; 14 | -------------------------------------------------------------------------------- /src-javascript/app/models/mongoose.user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | name: String, 6 | email: { 7 | type: String, 8 | unique: true, 9 | }, 10 | password: String, 11 | //add more fields 12 | }, 13 | { 14 | timestamps: true, 15 | } 16 | ); 17 | module.exports = mongoose.model('User', UserSchema); 18 | -------------------------------------------------------------------------------- /src-javascript/app/models/sequelize.user.model.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require('sequelize'); 2 | const { sequelize } = require('../../config/sequelizeConfig'); // Assuming sequelize connection is established and exported 3 | 4 | const User = sequelize.define( 5 | 'User', 6 | { 7 | name: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | }, 11 | email: { 12 | type: DataTypes.STRING, 13 | allowNull: false, 14 | unique: true, 15 | }, 16 | password: { 17 | type: DataTypes.STRING, 18 | allowNull: false, 19 | }, 20 | // add more fields as needed 21 | }, 22 | { 23 | timestamps: true, 24 | // Other configurations if required 25 | } 26 | ); 27 | 28 | module.exports = User; 29 | -------------------------------------------------------------------------------- /src-javascript/app/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | const RegisterUserController = require('../apis/user/controllers/register.user.controller'); 2 | const LoginUserController = require('../apis/user/controllers/login.user.controller'); 3 | 4 | module.exports = (app) => { 5 | RegisterUserController.post(app, '/api/v1/user/register/', []); 6 | LoginUserController.post(app, '/api/v1/user/login/', []); 7 | RegisterUserController.socketIO('hello'); 8 | }; 9 | -------------------------------------------------------------------------------- /src-javascript/app/utils/AsyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (fn) => (req, res, next) => { 2 | Promise.resolve(fn(req, res, next)).catch(next); 3 | }; 4 | module.exports = asyncHandler; 5 | -------------------------------------------------------------------------------- /src-javascript/app/utils/CronBuilder.js: -------------------------------------------------------------------------------- 1 | const { CronMonth, CronWeekday } = require('../enums/CronJob'); 2 | 3 | class CronBuilder { 4 | constructor() { 5 | this.fields = { 6 | second: '*', 7 | minute: '*', 8 | hour: '*', 9 | dayOfMonth: '*', 10 | month: '*', 11 | dayOfWeek: '*', 12 | }; 13 | } 14 | 15 | // Entry point for every() 16 | every() { 17 | return new Every(); 18 | } 19 | 20 | /** 21 | * @description Helper method to set the field value in the cron pattern 22 | * @param {string} field - Field name to be set 23 | * @param {string} value - Value to be set 24 | * @protected 25 | */ 26 | setField(field, value) { 27 | if (!['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'].includes(field)) { 28 | throw new Error('Invalid field name'); 29 | } 30 | this.fields[field] = value; 31 | } 32 | 33 | /** 34 | * @description Method to build the cron pattern 35 | * @private 36 | */ 37 | build() { 38 | return [ 39 | this.fields.second, 40 | this.fields.minute, 41 | this.fields.hour, 42 | this.fields.dayOfMonth, 43 | this.fields.month, 44 | this.fields.dayOfWeek, 45 | ].join(' '); 46 | } 47 | } 48 | 49 | class Every extends CronBuilder { 50 | /** 51 | * @description Method to set the seconds in the cron pattern 52 | * If `second()` is called without a parameter, it assumes "every second" 53 | * @param {number} [second] - Seconds 54 | */ 55 | second(second) { 56 | if (second === undefined) { 57 | this.setField('second', '*'); // Every second 58 | } else { 59 | if (second < 1 || second > 59) { 60 | throw new Error('Invalid value for seconds in Cron Pattern'); 61 | } 62 | this.setField('second', `*/${second}`); // Every N seconds 63 | } 64 | return this; 65 | } 66 | 67 | /** 68 | * @description Method to set the specific seconds in the cron pattern 69 | * @param {number[]} seconds - Array of seconds 70 | */ 71 | specificSeconds(seconds) { 72 | if (seconds.length === 0) { 73 | throw new Error('No seconds provided'); 74 | } 75 | 76 | if (seconds.some((second) => second < 0 || second > 59)) { 77 | throw new Error('Invalid value for seconds in Cron Pattern'); 78 | } 79 | const secondsString = seconds.join(','); 80 | this.setField('second', secondsString); 81 | return this; 82 | } 83 | 84 | /** 85 | * @description Method to set the seconds range in the cron pattern 86 | * @param {number} start - Start value 87 | * @param {number} end - End value 88 | */ 89 | secondBetween(start, end) { 90 | if (start < 0 || start > 59 || end < 0 || end > 59) { 91 | throw new Error('Invalid value for seconds in Cron Pattern'); 92 | } 93 | this.setField('second', `${start}-${end}`); 94 | return this; 95 | } 96 | 97 | /** 98 | * @description Method to set the minutes in the cron pattern 99 | * If `minute()` is called without a parameter, it assumes "every minute" 100 | * @param {number} [minute] - Minutes 101 | */ 102 | minute(minute) { 103 | if (minute === undefined) { 104 | this.setField('minute', '*'); // Every minute 105 | } else { 106 | if (minute < 0 || minute > 59) { 107 | throw new Error('Invalid value for minutes in Cron Pattern'); 108 | } 109 | this.setField('minute', `*/${minute}`); // Every N minutes 110 | } 111 | return this; 112 | } 113 | 114 | /** 115 | * @description Method to set the specific minutes in the cron pattern 116 | * @param {number[]} minutes - Array of minutes 117 | */ 118 | specificMinute(minutes) { 119 | if (minutes.length === 0) { 120 | throw new Error('No minutes provided'); 121 | } 122 | 123 | if (minutes.some((minute) => minute < 0 || minute > 59)) { 124 | throw new Error('Invalid value for minutes in Cron Pattern'); 125 | } 126 | const minutesString = minutes.join(','); 127 | this.setField('minute', minutesString); 128 | return this; 129 | } 130 | 131 | /** 132 | * @description Method to set the minutes range in the cron pattern 133 | * @param {number} start - Start value 134 | * @param {number} end - End value 135 | */ 136 | minuteBetween(start, end) { 137 | if (start < 0 || start > 59 || end < 0 || end > 59) { 138 | throw new Error('Invalid value for minutes in Cron Pattern'); 139 | } 140 | this.setField('minute', `${start}-${end}`); 141 | return this; 142 | } 143 | 144 | /** 145 | * @description Method to set the hours in the cron pattern 146 | * If `hour()` is called without a parameter, it assumes "every hour" 147 | * @param {number} [hour] - Hour 148 | */ 149 | hour(hour) { 150 | if (hour === undefined) { 151 | this.setField('hour', '*'); // Every hour 152 | } else { 153 | if (hour < 0 || hour > 23) { 154 | throw new Error('Invalid value for hours in Cron Pattern'); 155 | } 156 | this.setField('hour', `*/${hour}`); // Every N hours 157 | } 158 | return this; 159 | } 160 | 161 | /** 162 | * @description Method to set the specific hours in the cron pattern 163 | * @param {number[]} hours - Array of hours 164 | */ 165 | specificHour(hours) { 166 | if (hours.length === 0) { 167 | throw new Error('No hours provided'); 168 | } 169 | 170 | if (hours.some((hour) => hour < 0 || hour > 23)) { 171 | throw new Error('Invalid value for hours in Cron Pattern'); 172 | } 173 | const hoursString = hours.join(','); 174 | this.setField('hour', hoursString); 175 | return this; 176 | } 177 | 178 | /** 179 | * @description Method to set the hours range in the cron pattern 180 | * @param {number} start - Start value 181 | * @param {number} end - End value 182 | */ 183 | hourBetween(start, end) { 184 | if (start < 0 || start > 23 || end < 0 || end > 23) { 185 | throw new Error('Invalid value for hours in Cron Pattern'); 186 | } 187 | this.setField('hour', `${start}-${end}`); 188 | return this; 189 | } 190 | 191 | /** 192 | * @description Method to set the day of the month in the cron pattern 193 | * If `dayOfMonth()` is called without a parameter, it assumes "every day of the month" 194 | * @param {number} [n] - Number of days 195 | */ 196 | dayOfMonth(n) { 197 | if (n === undefined) { 198 | this.setField('dayOfMonth', '*'); // Every day of the month 199 | } else { 200 | if (n < 1 || n > 31) { 201 | throw new Error('Invalid value for day of the month in Cron Pattern'); 202 | } 203 | this.setField('dayOfMonth', `*/${n}`); // Every N days of the month 204 | } 205 | return this; 206 | } 207 | 208 | /** 209 | * @description Method to set the specific days of the month in the cron pattern 210 | * @param {number[]} n - Array of days 211 | */ 212 | specificDayOfMonth(n) { 213 | if (n.length === 0) { 214 | throw new Error('No days provided'); 215 | } 216 | 217 | if (n.some((day) => day < 1 || day > 31)) { 218 | throw new Error('Invalid value for days in Cron Pattern'); 219 | } 220 | const daysString = n.join(','); 221 | this.setField('dayOfMonth', daysString); 222 | return this; 223 | } 224 | 225 | /** 226 | * @description Method to set the days of the month range in the cron pattern 227 | * @param {number} start - Start value 228 | * @param {number} end - End value 229 | */ 230 | dayOfMonthBetween(start, end) { 231 | if (start < 1 || start > 31 || end < 1 || end > 31) { 232 | throw new Error('Invalid value for days in Cron Pattern'); 233 | } 234 | this.setField('dayOfMonth', `${start}-${end}`); 235 | return this; 236 | } 237 | 238 | /** 239 | * @description Method to set the day of the week in the cron pattern 240 | * If `dayOfWeek()` is called without a parameter, it assumes "every day of the week" 241 | * @param {CronWeekday} [day] - Day of the week 242 | */ 243 | dayOfWeek(day) { 244 | if (day === undefined) { 245 | this.setField('dayOfWeek', '*'); // Every day of the week 246 | } else { 247 | this.setField('dayOfWeek', `${day}`); 248 | } 249 | return this; 250 | } 251 | 252 | /** 253 | * @description Method to set the specific days of the week in the cron pattern 254 | * @param {CronWeekday[]} days - Array of days 255 | */ 256 | specificDayOfWeek(days) { 257 | if (days.length === 0) { 258 | throw new Error('No days provided'); 259 | } 260 | 261 | const daysString = days.map((day) => `${day}`).join(','); 262 | this.setField('dayOfWeek', daysString); 263 | return this; 264 | } 265 | 266 | /** 267 | * @description Method to set the month in the cron pattern 268 | * If `month()` is called without a parameter, it assumes "every month" 269 | * @param {CronMonth} [month] - Month 270 | */ 271 | month(month) { 272 | if (month === undefined) { 273 | this.setField('month', '*'); // Every month 274 | } else { 275 | this.setField('month', `${month}`); 276 | } 277 | return this; 278 | } 279 | 280 | /** 281 | * @description Method to set the specific months in the cron pattern 282 | * @param {CronMonth[]} months - Array of months 283 | */ 284 | specificMonth(months) { 285 | if (months.length === 0) { 286 | throw new Error('No months provided'); 287 | } 288 | 289 | const monthsString = months.map((month) => `${month}`).join(','); 290 | this.setField('month', monthsString); 291 | return this; 292 | } 293 | 294 | /** 295 | * @description Method to set the months range in the cron pattern 296 | * @param {CronMonth} start - Start value 297 | * @param {CronMonth} end - End value 298 | */ 299 | specificMonthBetween(start, end) { 300 | const startMonth = Object.values(CronMonth).indexOf(start); 301 | const endMonth = Object.values(CronMonth).indexOf(end); 302 | 303 | if (startMonth === -1 || endMonth === -1) { 304 | throw new Error('Invalid value for months in Cron Pattern'); 305 | } 306 | 307 | this.setField('month', `${start}-${end}`); 308 | return this; 309 | } 310 | } 311 | 312 | module.exports = CronBuilder; 313 | -------------------------------------------------------------------------------- /src-javascript/app/utils/EncryptionUtil.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const bcrypt = require('bcrypt'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | class EncryptionUtil { 6 | static async hashPassword(password, salt = 10) { 7 | return await bcrypt.hash(password, salt); 8 | } 9 | 10 | static async comparePassword(enteredPassword, dbPassword) { 11 | return await bcrypt.compare(enteredPassword, dbPassword); 12 | } 13 | 14 | static generateJwtTokens(data) { 15 | return { 16 | accessToken: jwt.sign(data, process.env.JWT_SECRET, { 17 | expiresIn: '2 days', 18 | }), 19 | refreshToken: jwt.sign(data, process.env.JWT_SECRET, { 20 | expiresIn: '10 days', 21 | }), 22 | }; 23 | } 24 | 25 | static verifyToken(token) { 26 | return jwt.verify(token, process.env.JWT_SECRET); 27 | } 28 | } 29 | 30 | module.exports = EncryptionUtil; 31 | -------------------------------------------------------------------------------- /src-javascript/app/utils/MasterController.js: -------------------------------------------------------------------------------- 1 | const { default: RequestBuilder, PayloadType } = require('./RequestBuilder'); 2 | const asyncHandler = require('./AsyncHandler'); 3 | const { default: SwaggerConfig, SwaggerMethod } = require('../../config/swaggerConfig'); 4 | const ResponseBuilder = require('./ResponseBuilder'); 5 | 6 | /** 7 | * @class MasterController 8 | * @description This class is used to create a controller class 9 | * @summary extends this class to create a controller class for a route or socket event 10 | * and override the restController or socketController method 11 | * to write the controller logic for the route or socket event respectively 12 | */ 13 | class MasterController { 14 | // start socket requests snippet 15 | static socketRequests = []; 16 | 17 | /** 18 | * @method MasterController.getSocketRequests 19 | * @description This static method is used to retrieve all the socket requests instances for a controller class. 20 | * 21 | * @returns {ISocketClient[]} - Returns an array of ISocketClient objects, each representing a socket request instance 22 | */ 23 | static getSocketRequests() { 24 | return this.socketRequests; 25 | } 26 | 27 | // end socket requests snippet 28 | 29 | // start cron jobs snippet 30 | static cronRequests = []; 31 | 32 | /** 33 | * @method MasterController.getCronRequests 34 | * @description This static method is used to retrieve all the cron job instances for a controller class. 35 | * 36 | * @returns {ICronJob[]} - Returns an array of ICronJob objects, each representing a cron job instance 37 | */ 38 | static getCronRequests() { 39 | return this.cronRequests; 40 | } 41 | 42 | // end cron jobs snippet 43 | 44 | /** 45 | * @method MasterController.doc 46 | * @description This method is used to get the swagger doc for a controller class 47 | * 48 | * @example 49 | * { 50 | * tags: ['User'], 51 | * summary: 'Register User', 52 | * description: 'Register User Description', 53 | * } 54 | * @returns {Object} swagger doc for a controller class {@link ISwaggerDoc} 55 | */ 56 | static doc() { 57 | return { 58 | tags: [], 59 | summary: '', 60 | description: '', 61 | }; 62 | } 63 | 64 | /** 65 | * @method MasterController.validate 66 | * @static 67 | * @description Creates a new RequestBuilder object configured for validating request payloads. 68 | * @returns {RequestBuilder} - RequestBuilder object configured for validation. 69 | * @example 70 | * // Create a payload validator using RequestBuilder 71 | * const payloadValidator = RequestBuilder.validate(); 72 | * // Add validation rules to the payload validator 73 | * payloadValidator.addToBody( 74 | * Joi.object().keys({ 75 | * email: Joi.string().email().required(), 76 | * password: Joi.string().min(8).max(20).required(), 77 | * }) 78 | * ); 79 | * payloadValidator.addToQuery( 80 | * Joi.object().keys({ 81 | * limit: Joi.number().required(), 82 | * offset: Joi.number().required(), 83 | * }) 84 | * ); 85 | * payloadValidator.addToParams( 86 | * Joi.object().keys({ 87 | * id: Joi.number().required(), 88 | * }) 89 | * ); 90 | * // Return the configured payload validator for further usage 91 | * return payloadValidator; 92 | */ 93 | static validate() { 94 | return new RequestBuilder(); 95 | } 96 | 97 | /** 98 | * @method MasterController.restController 99 | * @description Handles the controller logic after validating the request payload. 100 | * @param {Object} params - Parameters from the request URL. 101 | * @param {Object} query - Query parameters from the request URL. 102 | * @param {Object} body - Body content from the request. 103 | * @param {Object} headers - Headers from the request. 104 | * @param {Object} allData - Contains all data including params, query, body, headers, and custom data from middlewares. 105 | * @protected This method is protected and can only be accessed by the child class. 106 | * @returns {Promise} Promise resolving to any value representing the response. 107 | */ 108 | async restController(params, query, body, headers, allData) { 109 | // Controller logic goes here 110 | // Controller logic goes here 111 | console.log(params, query, body, headers, allData); 112 | // Return a ResponseBuilder instance 113 | return new ResponseBuilder(200, null, 'Success'); 114 | } 115 | 116 | /** 117 | * @method MasterController.socketController 118 | * @description Handles the logic for socket events. 119 | * @param {Server} io - Instance of the Socket.IO server. 120 | * @param {Socket} socket - Socket instance representing the client connection. 121 | * @param {any} payload - Payload data received from the client. 122 | * @protected This method is protected and can only be accessed by the child class. 123 | * @returns {any} Returns any value, usually the response or processing result. 124 | */ 125 | socketController(io, socket, payload) { 126 | // Logic for handling socket events goes here 127 | console.log(io, socket, payload); 128 | } 129 | 130 | /** 131 | * @method MasterController.cronController 132 | * @description Handles the logic for cron jobs. 133 | */ 134 | cronController() { 135 | // Implement cron job logic here 136 | console.log('Cron job executed'); 137 | } 138 | 139 | /** 140 | * @method MasterController.joiValidator 141 | * @description Validates the request payload based on provided validation rules. 142 | * @param {any} params - Parameters from the request URL. 143 | * @param {any} query - Query parameters from the request URL. 144 | * @param {any} body - Body content from the request. 145 | * @param {RequestBuilder} validationRules - Validation rules obtained from the validate method 146 | * (Joi validation rules are written in the validate method). 147 | * @private This method is private and can only be accessed by the MasterController class. 148 | * @returns {IJoiErrors | null} Returns an object containing any validation errors found, or null 149 | * if there are no errors. 150 | */ 151 | static joiValidator(params, query, body, validationRules) { 152 | // Check if there are no validation rules, return null (no validation needed) 153 | if (validationRules.get.length === 0) { 154 | return null; 155 | } 156 | // Object to store validation errors 157 | const joiErrors = { 158 | query: [], 159 | param: [], 160 | body: [], 161 | }; 162 | // Loop through each payload type and validate the corresponding data 163 | validationRules.payload.forEach((payload) => { 164 | if (payload.type === PayloadType.PARAMS) { 165 | // Validate params based on schema 166 | const schema = payload.schema; 167 | const { error } = schema.validate(params, { 168 | abortEarly: false, 169 | allowUnknown: true, 170 | }); 171 | if (error) { 172 | // Push validation error messages to the respective array 173 | joiErrors.param?.push(...error.details.map((err) => err.message)); 174 | } 175 | } else if (payload.type === PayloadType.QUERY) { 176 | // Validate query based on schema 177 | const schema = payload.schema; 178 | const { error } = schema.validate(query, { abortEarly: false, allowUnknown: true }); 179 | if (error) { 180 | // Push validation error messages to the respective array 181 | joiErrors.query?.push(...error.details.map((err) => err.message)); 182 | } 183 | } else if (payload.type === PayloadType.BODY) { 184 | // Validate body based on schema 185 | const schema = payload.schema; 186 | const { error } = schema.validate(body, { abortEarly: false, allowUnknown: true }); 187 | if (error) { 188 | // Push validation error messages to the respective array 189 | joiErrors.body?.push(...error.details.map((err) => err.message)); 190 | } 191 | } 192 | }); 193 | // Remove empty arrays from joiErrors 194 | if (joiErrors.query?.length === 0) delete joiErrors.query; 195 | if (joiErrors.param?.length === 0) delete joiErrors.param; 196 | if (joiErrors.body?.length === 0) delete joiErrors.body; 197 | // Return null if no errors, i.e. all arrays are removed above 198 | if (Object.keys(joiErrors).length === 0) return null; 199 | return joiErrors; 200 | } 201 | 202 | /** 203 | * @method MasterController.handler 204 | * @description Handles the request and response. 205 | * @private This method is private and can only be accessed by the MasterController class. 206 | * @returns {RequestHandler} Returns an Express RequestHandler function. 207 | */ 208 | static handler() { 209 | // Using 'self' to access the class within the async function scope 210 | const self = this; 211 | // Returns an async function serving as a RequestHandler for Express 212 | return asyncHandler(async (req, res) => { 213 | // Create a new instance of the current class 214 | const controller = new self(); 215 | // Combine all request data into a single object 216 | const allData = { ...req.params, ...req.query, ...req.body, ...req.headers, ...req }; 217 | // Retrieve validation rules using 'validate' method 218 | const validationRules = this.validate(); 219 | // Perform payload validation and capture any validation errors 220 | const joiErrors = this.joiValidator(req.params, req.query, req.body, validationRules); 221 | // If there are validation errors, respond with a 400 status and the error details 222 | if (joiErrors) { 223 | return res.status(400).json({ 224 | status: 400, 225 | message: 'Validation Error', 226 | data: null, 227 | errors: joiErrors, 228 | }); 229 | } 230 | // Invoke the 'restController' method to handle the request and get the response 231 | const { response } = await controller.restController( 232 | req.params, 233 | req.query, 234 | req.body, 235 | req.headers, 236 | allData 237 | ); 238 | // Respond with the status and data from 'restController' method 239 | res.status(response.status).json(response); 240 | }); 241 | } 242 | 243 | /** 244 | * @method MasterController.get 245 | * @description Registers a GET route for the controller class. 246 | * @param {Router} router - Router object. 247 | * @param {string} path - Path for the route. 248 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 249 | * @returns {Router} Router object with the registered GET route. 250 | */ 251 | static get(router, path, middlewares) { 252 | SwaggerConfig.recordApi(path, SwaggerMethod.GET, this); 253 | return router.get(path, middlewares, this.handler()); 254 | } 255 | 256 | /** 257 | * @method MasterController.post 258 | * @description Registers a POST route for the controller class. 259 | * @param {Router} router - Router object. 260 | * @param {string} path - Path for the route. 261 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 262 | * @returns {Router} Router object with the registered POST route. 263 | */ 264 | static post(router, path, middlewares) { 265 | SwaggerConfig.recordApi(path, SwaggerMethod.POST, this); 266 | return router.post(path, middlewares, this.handler()); 267 | } 268 | 269 | /** 270 | * @method MasterController.put 271 | * @description Registers a PUT route for the controller class. 272 | * @param {Router} router - Router object. 273 | * @param {string} path - Path for the route. 274 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 275 | * @returns {Router} Router object with the registered PUT route. 276 | */ 277 | static put(router, path, middlewares) { 278 | SwaggerConfig.recordApi(path, SwaggerMethod.PUT, this); 279 | return router.put(path, middlewares, this.handler()); 280 | } 281 | 282 | /** 283 | * @method MasterController.delete 284 | * @description Registers a DELETE route for the controller class. 285 | * @param {Router} router - Router object. 286 | * @param {string} path - Path for the route. 287 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 288 | * @returns {Router} Router object with the registered DELETE route. 289 | */ 290 | static delete(router, path, middlewares) { 291 | SwaggerConfig.recordApi(path, SwaggerMethod.DELETE, this); 292 | return router.delete(path, middlewares, this.handler()); 293 | } 294 | 295 | /** 296 | * @method MasterController.patch 297 | * @description Registers a PATCH route for the controller class. 298 | * @param {Router} router - Router object. 299 | * @param {string} path - Path for the route. 300 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 301 | * @returns {Router} Router object with the registered PATCH route. 302 | */ 303 | static patch(router, path, middlewares) { 304 | SwaggerConfig.recordApi(path, SwaggerMethod.PATCH, this); 305 | return router.patch(path, middlewares, this.handler()); 306 | } 307 | 308 | /** 309 | * @method MasterController.socketIO 310 | * @description Registers a socket event for the controller class. 311 | * @param {string} event - Event name. 312 | */ 313 | static socketIO(event) { 314 | this.socketRequests.push({ event, masterController: new this() }); 315 | } 316 | 317 | /** 318 | * @method MasterController.cronJob 319 | * @description Registers a cron job for the controller class. 320 | * @param {string} cronPattern - Cron pattern for the job. 321 | */ 322 | static cronJob(cronPattern) { 323 | this.cronRequests.push({ cronPattern, masterController: new this() }); 324 | } 325 | } 326 | 327 | module.exports = MasterController; 328 | -------------------------------------------------------------------------------- /src-javascript/app/utils/RequestBuilder.js: -------------------------------------------------------------------------------- 1 | const PayloadType = { 2 | PARAMS: 0, 3 | QUERY: 1, 4 | BODY: 2, 5 | }; 6 | 7 | class RequestBuilder { 8 | constructor() { 9 | this.payload = []; 10 | } 11 | 12 | addToParams(payload) { 13 | this.payload.push({ type: PayloadType.PARAMS, schema: payload }); 14 | } 15 | 16 | addToQuery(payload) { 17 | this.payload.push({ type: PayloadType.QUERY, schema: payload }); 18 | } 19 | 20 | addToBody(payload) { 21 | this.payload.push({ type: PayloadType.BODY, schema: payload }); 22 | } 23 | 24 | get get() { 25 | return this.payload; 26 | } 27 | } 28 | 29 | module.exports.PayloadType = PayloadType; 30 | module.exports.default = RequestBuilder; 31 | -------------------------------------------------------------------------------- /src-javascript/app/utils/ResponseBuilder.js: -------------------------------------------------------------------------------- 1 | module.exports = class ResponseBuilder { 2 | response; 3 | 4 | constructor(status, data, message) { 5 | this.response = { 6 | status, 7 | data, 8 | message, 9 | }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src-javascript/config/cronConfig.js: -------------------------------------------------------------------------------- 1 | const MasterController = require('../app/utils/MasterController'); 2 | const asyncHandler = require('../app/utils/asyncHandler'); 3 | const { CronJob } = require('cron'); 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | 7 | class CronConfig { 8 | /** 9 | * @description Method to initialize the cron jobs 10 | * @param dir - The directory to search for cron jobs 11 | */ 12 | static InitCronJobs = async (dir) => { 13 | const entries = await fs.readdir(dir, { withFileTypes: true }); 14 | 15 | for (const entry of entries) { 16 | const fullPath = path.join(dir, entry.name); 17 | 18 | if (entry.isDirectory()) { 19 | await CronConfig.InitCronJobs(fullPath); 20 | } else if ( 21 | entry.isFile() && 22 | (entry.name.endsWith('.cron.ts') || entry.name.endsWith('.cron.js')) 23 | ) { 24 | require(fullPath); 25 | } 26 | } 27 | }; 28 | 29 | /** 30 | * @description Method to start the cron jobs for the registered crons 31 | */ 32 | static startCronJobs = () => { 33 | MasterController.getCronRequests().forEach((client) => { 34 | asyncHandler( 35 | (async () => { 36 | const cron = new CronJob(client.cronPattern, () => { 37 | client.masterController.cronController(); 38 | }); 39 | cron.start(); 40 | })() 41 | ); 42 | }); 43 | }; 44 | } 45 | 46 | module.exports = CronConfig; 47 | -------------------------------------------------------------------------------- /src-javascript/config/expressConfig.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const morgan = require('morgan'); 4 | const joiErrorHandler = require('../app/handlers/JoiErrorHandler'); 5 | const customErrorHandler = require('../app/handlers/CustomErrorHandler'); 6 | const fs = require('fs').promises; 7 | const path = require('path'); 8 | // start swagger import 9 | const swaggerUI = require('swagger-ui-express'); 10 | const { default: SwaggerConfig } = require('./swaggerConfig'); 11 | // end swagger import 12 | 13 | const server = async () => { 14 | const app = express(); 15 | app.use(express.static('public')); 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: true })); 18 | app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); 19 | app.use( 20 | cors({ 21 | origin: '*', 22 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 23 | allowedHeaders: [ 24 | 'Content-Type', 25 | 'Authorization', 26 | 'X-Requested-With', 27 | 'Accept', 28 | 'Origin', 29 | 'Access-Control-Allow-Headers', 30 | 'Access-Control-Allow-Origin', 31 | 'Access-Control-Allow-Methods', 32 | 'Accept-Encoding', 33 | 'Accept-Language', 34 | 'Cache-Control', 35 | 'Connection', 36 | 'Content-Length', 37 | 'Host', 38 | 'Pragma', 39 | 'Referer', 40 | 'User-Agent', 41 | 'X-Forwarded-For', 42 | 'X-Forwarded-Proto', 43 | ], 44 | exposedHeaders: [ 45 | 'Content-Type', 46 | 'Authorization', 47 | 'X-Requested-With', 48 | 'Accept', 49 | 'Origin', 50 | 'Access-Control-Allow-Headers', 51 | 'Access-Control-Allow-Origin', 52 | 'Access-Control-Allow-Methods', 53 | 'Accept-Encoding', 54 | 'Accept-Language', 55 | 'Cache-Control', 56 | 'Connection', 57 | 'Content-Length', 58 | 'Host', 59 | 'Pragma', 60 | 'Referer', 61 | 'User-Agent', 62 | 'X-Forwarded-For', 63 | 'X-Forwarded-Proto', 64 | ], 65 | optionsSuccessStatus: 204, 66 | credentials: true, 67 | preflightContinue: false, 68 | }) 69 | ); 70 | 71 | const loadRouters = async (dir) => { 72 | //load all routers from dir and sub dir 73 | const entries = await fs.readdir(dir, { withFileTypes: true }); 74 | for (const entry of entries) { 75 | const fullPath = path.join(dir, entry.name); 76 | if (entry.isDirectory()) { 77 | //recursive call to sub dir 78 | await loadRouters(fullPath); 79 | } else if ( 80 | entry.isFile() && 81 | (entry.name.endsWith('.routes.ts') || entry.name.endsWith('.routes.js')) 82 | ) { 83 | const router = require(fullPath); 84 | //to support both default exports in commonjs and es6 85 | if (router.default) router.default(app); 86 | else router(app); 87 | } 88 | } 89 | }; 90 | 91 | // start swagger config 92 | SwaggerConfig.initSwagger({ 93 | title: 'Node Swagger API', 94 | description: 'Demonstrating how to describe a RESTful API with Swagger', 95 | version: '1.0.0', 96 | swaggerDocPath: path.join(__dirname, '../../swagger.json'), 97 | modifySwaggerDoc: false, 98 | }); 99 | // end swagger config 100 | 101 | await loadRouters(path.join(__dirname, '../app/routes')); 102 | 103 | app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(SwaggerConfig.getSwaggerDocument())); 104 | 105 | app.use(joiErrorHandler, customErrorHandler, (err, _req, res, _next) => { 106 | console.error(err); // Log the error for debugging 107 | return res.status(500).json({ error: 'Internal Server Error' }); // Respond with a 500 Internal Server Error 108 | }); 109 | 110 | // no route found 111 | app.use((_req, res) => { 112 | return res.status(404).json({ error: 'Route Not Found' }); 113 | }); 114 | 115 | return app; 116 | }; 117 | 118 | module.exports = server; 119 | -------------------------------------------------------------------------------- /src-javascript/config/mongooseConfig.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const process = require('process'); 3 | require('dotenv').config(); 4 | 5 | const mongooseOptions = { 6 | maxPoolSize: 10, 7 | serverSelectionTimeoutMS: 5000, 8 | socketTimeoutMS: 45000, 9 | }; 10 | 11 | const mongooseConnect = async () => { 12 | try { 13 | await mongoose.connect(process.env.MONGO_URI, mongooseOptions); 14 | console.log('\x1b[32m%s\x1b[0m', 'Database Connected successfully.'); 15 | } catch (err) { 16 | console.error('Unable to connect to the database:', err); 17 | throw err; 18 | } 19 | }; 20 | 21 | exports.mongooseConnect = mongooseConnect; 22 | -------------------------------------------------------------------------------- /src-javascript/config/sequelizeConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config(); 3 | const { Op, Sequelize } = require('sequelize'); 4 | const sequelizeOptions = { 5 | database: process.env.DB_NAME, 6 | username: process.env.DB_USER, 7 | password: process.env.DB_PASS, 8 | host: process.env.DB_HOST, 9 | port: Number(process.env.DB_PORT), 10 | dialect: 'postgres', 11 | dialectOptions: { 12 | ssl: false, 13 | }, 14 | models: [path.join(__dirname, '..', 'app', 'models')], 15 | logging: console.log, 16 | operatorsAliases: { 17 | $eq: Op.eq, 18 | $ne: Op.ne, 19 | $gte: Op.gte, 20 | $gt: Op.gt, 21 | $lte: Op.lte, 22 | $lt: Op.lt, 23 | $not: Op.not, 24 | $in: Op.in, 25 | $notIn: Op.notIn, 26 | $is: Op.is, 27 | $like: Op.like, 28 | $notLike: Op.notLike, 29 | $iLike: Op.iLike, 30 | $notILike: Op.notILike, 31 | $regexp: Op.regexp, 32 | $notRegexp: Op.notRegexp, 33 | $iRegexp: Op.iRegexp, 34 | $notIRegexp: Op.notIRegexp, 35 | $between: Op.between, 36 | $notBetween: Op.notBetween, 37 | $overlap: Op.overlap, 38 | $contains: Op.contains, 39 | $contained: Op.contained, 40 | $adjacent: Op.adjacent, 41 | $strictLeft: Op.strictLeft, 42 | $strictRight: Op.strictRight, 43 | $noExtendRight: Op.noExtendRight, 44 | $noExtendLeft: Op.noExtendLeft, 45 | $and: Op.and, 46 | $or: Op.or, 47 | $any: Op.any, 48 | $all: Op.all, 49 | $values: Op.values, 50 | $col: Op.col, 51 | }, 52 | pool: { 53 | max: 5, 54 | min: 0, 55 | acquire: 30000, 56 | idle: 10000, 57 | }, 58 | retry: { 59 | match: [ 60 | /ETIMEDOUT/, 61 | /EHOSTUNREACH/, 62 | /ECONNRESET/, 63 | /ECONNREFUSED/, 64 | /ETIMEDOUT/, 65 | /ESOCKETTIMEDOUT/, 66 | /EHOSTUNREACH/, 67 | /EPIPE/, 68 | /EAI_AGAIN/, 69 | /SequelizeConnectionError/, 70 | /SequelizeConnectionRefusedError/, 71 | /SequelizeHostNotFoundError/, 72 | /SequelizeHostNotReachableError/, 73 | /SequelizeInvalidConnectionError/, 74 | /SequelizeConnectionTimedOutError/, 75 | ], 76 | max: 3, 77 | }, 78 | }; 79 | const sequelize = new Sequelize(sequelizeOptions); 80 | 81 | const sequelizeConnect = async () => { 82 | try { 83 | await sequelize.authenticate(); 84 | console.log('\x1b[32m%s\x1b[0m', 'Database Connected successfully.'); 85 | await sequelize.sync({ alter: false }); 86 | console.log('\x1b[32m%s\x1b[0m', 'Database Synced successfully.'); 87 | } catch (err) { 88 | console.error('Unable to connect to the database:', err); 89 | throw err; 90 | } 91 | }; 92 | 93 | exports.sequelize = sequelize; 94 | exports.sequelizeConnect = sequelizeConnect; 95 | -------------------------------------------------------------------------------- /src-javascript/config/socketConfig.js: -------------------------------------------------------------------------------- 1 | const { Server } = require('socket.io'); 2 | const MasterController = require('../app/utils/MasterController'); 3 | const asyncHandler = require('../app/utils/AsyncHandler'); 4 | 5 | class SocketConfig { 6 | /** 7 | * @description Method to initialize the socket io instance 8 | * @param server http server instance 9 | */ 10 | static init(server) { 11 | return new Server(server, { 12 | /* Socket.IO options (if needed) */ 13 | cors: { 14 | origin: '*', 15 | methods: ['GET', 'POST'], 16 | }, 17 | }); 18 | } 19 | 20 | /** 21 | * @description This method is used to handle the socket connection and listen for events 22 | * @param io socket io instance 23 | * @param socket socket instance 24 | */ 25 | static socketListener(io, socket) { 26 | console.log('New client connected'); 27 | socket.on('disconnect', () => { 28 | console.log('Client disconnected'); 29 | }); 30 | MasterController.getSocketRequests().forEach((client) => { 31 | socket.on(client.event, (payload) => { 32 | asyncHandler( 33 | (async () => { 34 | client.masterController.socketController(io, socket, payload); 35 | })() 36 | ); 37 | }); 38 | }); 39 | } 40 | } 41 | 42 | module.exports = SocketConfig; 43 | -------------------------------------------------------------------------------- /src-javascript/config/swaggerConfig.js: -------------------------------------------------------------------------------- 1 | const { PayloadType } = require('../app/utils/RequestBuilder'); 2 | const j2s = require('joi-to-swagger'); 3 | const fs = require('fs').promises; 4 | 5 | const SwaggerMethod = { 6 | GET: 'get', 7 | POST: 'post', 8 | PUT: 'put', 9 | DELETE: 'delete', 10 | PATCH: 'patch', 11 | }; 12 | 13 | class SwaggerConfig { 14 | static swaggerDocument; 15 | static swaggerPath; 16 | static swaggerModify; 17 | 18 | static initSwagger(options) { 19 | const { title, description, version, swaggerDocPath, modifySwaggerDoc } = options; 20 | if (swaggerDocPath) { 21 | this.swaggerPath = swaggerDocPath; 22 | this.swaggerModify = modifySwaggerDoc; 23 | this.swaggerDocument = require(swaggerDocPath); 24 | this.swaggerDocument.paths = {}; 25 | if (this.swaggerModify) { 26 | this.modifySwaggerDocument(); 27 | } 28 | } else { 29 | this.swaggerDocument = { 30 | swagger: '2.0', 31 | info: { 32 | title, 33 | description, 34 | version, 35 | }, 36 | schemes: ['http', 'https'], 37 | consumes: ['application/json'], 38 | produces: ['application/json'], 39 | securityDefinitions: { 40 | token: { 41 | type: 'apiKey', 42 | name: 'Authorization', 43 | in: 'header', 44 | }, 45 | }, 46 | paths: {}, 47 | }; 48 | } 49 | } 50 | 51 | static getSwaggerDocument() { 52 | return this.swaggerDocument; 53 | } 54 | 55 | static swaggerDocsFromJoiSchema(validationRules) { 56 | let parameters = []; 57 | validationRules.payload.forEach((payload) => { 58 | if (payload.type === PayloadType.PARAMS) { 59 | const schema = payload.schema; 60 | const { swagger } = j2s(schema); 61 | for (const key in swagger.properties) { 62 | const property = swagger.properties[key]; 63 | const parameter = { 64 | name: key, 65 | in: 'path', 66 | required: swagger.required?.includes(key) ?? false, 67 | type: property.type, 68 | format: property.format, 69 | }; 70 | parameters.push(parameter); 71 | } 72 | } else if (payload.type === PayloadType.QUERY) { 73 | const schema = payload.schema; 74 | const { swagger } = j2s(schema); 75 | for (const key in swagger.properties) { 76 | const property = swagger.properties[key]; 77 | const parameter = { 78 | name: key, 79 | in: 'query', 80 | required: swagger.required?.includes(key) ?? false, 81 | type: property.type, 82 | format: property.format, 83 | }; 84 | parameters.push(parameter); 85 | } 86 | } else if (payload.type === PayloadType.BODY) { 87 | const schema = payload.schema; 88 | const { swagger } = j2s(schema); 89 | const parameter = { 90 | name: 'body', 91 | in: 'body', 92 | required: true, 93 | schema: swagger, 94 | }; 95 | parameters.push(parameter); 96 | } 97 | }); 98 | return parameters; 99 | } 100 | 101 | static recordApi(path, method, currentRef) { 102 | const key = path.replace(/:(\w+)/g, '{$&}').replace(/:/g, ''); 103 | const parameters = this.swaggerDocsFromJoiSchema(currentRef.validate()); 104 | const paths = this.swaggerDocument.paths; 105 | const pathObj = paths[key] || {}; 106 | const methodObj = pathObj[method] || { 107 | tags: [], 108 | summary: '', 109 | description: '', 110 | produces: ['application/json'], 111 | responses: this.exampleResponses(), 112 | }; 113 | methodObj.parameters = parameters; 114 | methodObj.tags = currentRef.doc().tags; 115 | methodObj.summary = currentRef.doc().summary; 116 | methodObj.description = currentRef.doc().description; 117 | methodObj.operationId = currentRef.name; 118 | pathObj[method] = methodObj; 119 | paths[key] = pathObj; 120 | this.swaggerDocument.paths = paths; 121 | if (this.swaggerModify) { 122 | this.modifySwaggerDocument(); 123 | } 124 | } 125 | 126 | static modifySwaggerDocument() { 127 | fs.writeFile(this.swaggerPath, JSON.stringify(this.swaggerDocument, null, 2), { 128 | flag: 'w', 129 | }) 130 | .then(() => { 131 | console.log('Swagger document updated'); 132 | }) 133 | .catch((err) => { 134 | console.log('Error updating swagger document', err); 135 | }); 136 | } 137 | 138 | static exampleResponses() { 139 | return { 140 | 200: { 141 | description: 'OK', 142 | schema: { 143 | type: 'object', 144 | properties: { 145 | status: { 146 | type: 'string', 147 | example: 'success', 148 | }, 149 | data: { 150 | type: 'object', 151 | }, 152 | message: { 153 | type: 'string', 154 | example: 'Success', 155 | }, 156 | }, 157 | }, 158 | }, 159 | 400: { 160 | description: 'Bad Request', 161 | schema: { 162 | type: 'object', 163 | properties: { 164 | status: { 165 | type: 'string', 166 | example: 'error', 167 | }, 168 | data: { 169 | type: 'object', 170 | }, 171 | message: { 172 | type: 'string', 173 | example: 'Bad Request', 174 | }, 175 | errors: { 176 | type: 'object', 177 | }, 178 | }, 179 | }, 180 | }, 181 | 404: { 182 | description: 'Not Found', 183 | schema: { 184 | type: 'object', 185 | properties: { 186 | status: { 187 | type: 'string', 188 | example: 'error', 189 | }, 190 | data: { 191 | type: 'object', 192 | }, 193 | message: { 194 | type: 'string', 195 | example: 'Not Found', 196 | }, 197 | }, 198 | }, 199 | }, 200 | 500: { 201 | description: 'Internal Server Error', 202 | schema: { 203 | type: 'object', 204 | properties: { 205 | status: { 206 | type: 'string', 207 | example: 'error', 208 | }, 209 | data: { 210 | type: 'object', 211 | }, 212 | message: { 213 | type: 'string', 214 | example: 'Internal Server Error', 215 | }, 216 | }, 217 | }, 218 | }, 219 | }; 220 | } 221 | } 222 | 223 | module.exports.SwaggerMethod = SwaggerMethod; 224 | module.exports.default = SwaggerConfig; 225 | -------------------------------------------------------------------------------- /src-javascript/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const serverConfig = require('./config/expressConfig'); 3 | const process = require('process'); 4 | const { mongooseConnect } = require('./config/mongooseConfig'); 5 | const { sequelizeConnect } = require('./config/sequelizeConfig'); 6 | const SocketConfig = require('./config/socketConfig'); 7 | const CronConfig = require('./config/cronConfig'); 8 | const path = require('path'); 9 | 10 | require('dotenv').config(); 11 | 12 | const port = process.env.PORT || 3000; 13 | 14 | (async () => { 15 | const app = await serverConfig(); 16 | 17 | // Getting the dialect from .env file 18 | if (!process.env.DB_DIALECT) { 19 | throw new Error('DB_DIALECT not found in .env file'); 20 | } 21 | 22 | // start if dialect valid 23 | if ( 24 | ![ 25 | 'postgres', 26 | 'mysql', 27 | 'mariadb', 28 | 'sqlite', 29 | 'mssql', 30 | 'db2', 31 | 'snowflake', 32 | 'oracle', 33 | 'mongodb', 34 | ].includes(process.env.DB_DIALECT) 35 | ) { 36 | throw new Error('DB_DIALECT must be either postgres, mysql, mariadb, sqlite or mongodb'); 37 | } 38 | // end if dialect valid 39 | 40 | // Connect to the database 41 | // start if sequelize dialect check 42 | if ( 43 | ['postgres', 'mysql', 'mariadb', 'sqlite', 'mssql', 'db2', 'snowflake', 'oracle'].includes( 44 | process.env.DB_DIALECT 45 | ) 46 | ) { 47 | try { 48 | await sequelizeConnect(); 49 | } catch (err) { 50 | console.error('Unable to connect to the database:', err); 51 | throw err; 52 | } 53 | } 54 | // end if sequelize dialect check 55 | 56 | // start if mongoose dialect check 57 | else if (process.env.DB_DIALECT === 'mongodb') { 58 | try { 59 | await mongooseConnect(); 60 | } catch (err) { 61 | console.error('Unable to connect to the database:', err); 62 | throw err; 63 | } 64 | } 65 | // end if mongoose dialect check 66 | 67 | // Create an HTTP server instance 68 | const httpServer = http.createServer(app); 69 | // Initialize Socket.IO with the HTTP server 70 | const io = SocketConfig.init(httpServer); 71 | 72 | io.on('connection', (socket) => { 73 | SocketConfig.socketListener(io, socket); 74 | }); 75 | // End Initialize Socket.IO 76 | 77 | // Initialize Cron Jobs 78 | await CronConfig.InitCronJobs(path.join(__dirname, 'app/crons')); 79 | CronConfig.startCronJobs(); 80 | 81 | // End Initialize Cron Jobs 82 | 83 | // Start listening for HTTP requests 84 | httpServer.listen(port, () => { 85 | console.log(`Server is listening on port ${port}`); 86 | }); 87 | // End listening for HTTP requests 88 | })(); 89 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/controllers/login.user.controller.ts: -------------------------------------------------------------------------------- 1 | import MasterController from '../../../utils/MasterController'; 2 | import RequestBuilder from '../../../utils/RequestBuilder'; 3 | import Joi from 'joi'; 4 | import { ILoginUser } from '../interfaces'; 5 | import userService from '../services/user.service'; 6 | import ResponseBuilder from '../../../utils/ResponseBuilder'; 7 | import { StatusCodes } from '../../../enums/StatusCodes'; 8 | 9 | export default class LoginUserController extends MasterController { 10 | static doc() { 11 | return { 12 | tags: ['User'], 13 | summary: 'Login User', 14 | description: 'Login User', 15 | }; 16 | } 17 | 18 | public static validate() { 19 | const payload = new RequestBuilder(); 20 | 21 | payload.addToBody( 22 | Joi.object().keys({ 23 | email: Joi.string().email().required(), 24 | password: Joi.string().min(8).max(20).required(), 25 | }) 26 | ); 27 | 28 | return payload; 29 | } 30 | 31 | async restController( 32 | params: null, 33 | query: null, 34 | body: ILoginUser, 35 | headers: any, 36 | allData: any 37 | ): Promise { 38 | const { email, password } = body; 39 | 40 | const response = await userService.loginUser({ email, password }); 41 | 42 | return new ResponseBuilder(StatusCodes.SUCCESS, response, 'User logged in successfully'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/controllers/register.user.controller.ts: -------------------------------------------------------------------------------- 1 | import MasterController from '../../../utils/MasterController'; 2 | import { StatusCodes } from '../../../enums/StatusCodes'; 3 | import ResponseBuilder from '../../../utils/ResponseBuilder'; 4 | import RequestBuilder from '../../../utils/RequestBuilder'; 5 | import Joi from 'joi'; 6 | import userService from '../services/user.service'; 7 | import { IRegisterUser } from '../interfaces'; 8 | 9 | export default class RegisterUserController extends MasterController { 10 | static doc() { 11 | return { 12 | tags: ['User'], 13 | summary: 'Register User', 14 | description: 'Register User', 15 | }; 16 | } 17 | 18 | public static validate(): RequestBuilder { 19 | const payload = new RequestBuilder(); 20 | 21 | payload.addToBody( 22 | Joi.object().keys({ 23 | name: Joi.string().required(), 24 | email: Joi.string().email().required(), 25 | password: Joi.string().min(8).max(20).required(), 26 | }) 27 | ); 28 | 29 | return payload; 30 | } 31 | 32 | async restController( 33 | params: null, 34 | query: null, 35 | body: IRegisterUser, 36 | headers: any, 37 | allData: any 38 | ): Promise { 39 | const { name, email, password } = body; 40 | 41 | const response = await userService.registerUser({ name, email, password }); 42 | 43 | return new ResponseBuilder(StatusCodes.SUCCESS, response, 'User registered successfully'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IRegisterUser { 2 | name: string; 3 | email: string; 4 | password: string; 5 | } 6 | 7 | export interface ILoginUser { 8 | email: string; 9 | password: string; 10 | } 11 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/repositories/mongoose.user.repository.ts: -------------------------------------------------------------------------------- 1 | import User from '../../../models/mongoose.user.model'; 2 | 3 | class UserRepository { 4 | async find(filter: {}) { 5 | return User.findOne(filter).exec(); 6 | } 7 | 8 | async create(data: any) { 9 | return User.create(data); 10 | } 11 | } 12 | 13 | export default new UserRepository(); 14 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/repositories/sequelize.user.repository.ts: -------------------------------------------------------------------------------- 1 | import User from '../../../models/sequelize.user.model'; 2 | 3 | class UserRepository { 4 | async find(filter: {}) { 5 | return User.findOne({ 6 | where: filter, 7 | }); 8 | } 9 | 10 | async create(data: any) { 11 | return User.create(data); 12 | } 13 | } 14 | 15 | export default new UserRepository(); 16 | -------------------------------------------------------------------------------- /src-typescript/app/apis/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { ILoginUser, IRegisterUser } from '../interfaces'; 2 | import userRepository from '../repositories/sequelize.user.repository'; 3 | import { ValidationError } from '../../../handlers/CustomErrorHandler'; 4 | import EncryptionUtil from '../../../utils/EncryptionUtil'; 5 | 6 | class UserService { 7 | async registerUser(data: IRegisterUser) { 8 | const user = await userRepository.find({ email: data.email }); 9 | 10 | if (user) { 11 | throw new ValidationError('User already exists'); 12 | } 13 | 14 | data.password = await EncryptionUtil.hashPassword(data.password); 15 | 16 | return userRepository.create(data); 17 | } 18 | 19 | async loginUser(data: ILoginUser) { 20 | const user = await userRepository.find({ email: data.email }); 21 | 22 | if (!user) { 23 | throw new ValidationError('User does not exist'); 24 | } 25 | 26 | const isPasswordValid = await EncryptionUtil.comparePassword( 27 | data.password, 28 | user.password ?? '' 29 | ); 30 | 31 | if (!isPasswordValid) { 32 | throw new ValidationError('Invalid password'); 33 | } 34 | 35 | return user; 36 | } 37 | } 38 | 39 | export default new UserService(); 40 | -------------------------------------------------------------------------------- /src-typescript/app/common/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IAccessToken { 2 | accessToken: string; 3 | refreshToken: string; 4 | } 5 | -------------------------------------------------------------------------------- /src-typescript/app/crons/demo.cron.ts: -------------------------------------------------------------------------------- 1 | import MasterController from '../utils/MasterController'; 2 | import CronBuilder from '../utils/CronBuilder'; 3 | import { CronWeekday } from '../enums/CronJob'; 4 | 5 | class DemoCron extends MasterController { 6 | cronController() { 7 | console.log('Cron job is running'); 8 | } 9 | } 10 | 11 | // Unix Crontab format 12 | DemoCron.cronJob('*/5 * * * * *'); 13 | 14 | // Using CronBuilder 15 | DemoCron.cronJob( 16 | new CronBuilder() 17 | .every() 18 | .second() 19 | .every() 20 | .specificMinute([10, 20, 30]) 21 | .every() 22 | .dayOfMonth() 23 | .every() 24 | .dayOfWeek(CronWeekday.Friday) 25 | .build() 26 | ); 27 | -------------------------------------------------------------------------------- /src-typescript/app/enums/CronJob.ts: -------------------------------------------------------------------------------- 1 | export enum CronMonth { 2 | January = 'jan', 3 | February = 'feb', 4 | March = 'mar', 5 | April = 'apr', 6 | May = 'may', 7 | June = 'jun', 8 | July = 'jul', 9 | August = 'aug', 10 | September = 'sep', 11 | October = 'oct', 12 | November = 'nov', 13 | December = 'dec', 14 | } 15 | 16 | export enum CronWeekday { 17 | Sunday = 'sun', 18 | Monday = 'mon', 19 | Tuesday = 'tue', 20 | Wednesday = 'wed', 21 | Thursday = 'thu', 22 | Friday = 'fri', 23 | Saturday = 'sat', 24 | } 25 | -------------------------------------------------------------------------------- /src-typescript/app/enums/StatusCodes.ts: -------------------------------------------------------------------------------- 1 | export enum StatusCodes { 2 | SUCCESS = 200, 3 | CREATED = 201, 4 | UNAUTHORISED = 401, 5 | CONFLICT = 409, 6 | BAD_REQUEST = 400, 7 | SERVER_ERROR = 500, 8 | NOT_FOUND = 404, 9 | ALREADY_EXISTS = 403, 10 | } 11 | -------------------------------------------------------------------------------- /src-typescript/app/handlers/CustomErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { StatusCodes } from '../enums/StatusCodes'; 3 | 4 | export class ValidationError extends Error { 5 | errorCode?: number; 6 | 7 | constructor(message: string, errorCode?: number) { 8 | super(message); 9 | this.name = 'ValidationError'; 10 | this.errorCode = errorCode; 11 | } 12 | } 13 | 14 | const customErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { 15 | if (err.name === 'ValidationError') { 16 | // Handle ValidationError 17 | return res.status(err.errorCode || StatusCodes.BAD_REQUEST).json({ error: err.message }); // Respond with a 400 Bad Request and the error message 18 | } else { 19 | next(err); // Pass other errors to the default error handler 20 | } 21 | }; 22 | 23 | export default customErrorHandler; 24 | -------------------------------------------------------------------------------- /src-typescript/app/handlers/JoiErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import Joi from 'joi'; 3 | 4 | const joiErrorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { 5 | if (err instanceof Joi.ValidationError) { 6 | // Joi validation error occurred 7 | const errorMessage = err.details.map((error) => error.message).join(', '); 8 | res.status(400).json({ error: errorMessage }); // Respond with a 400 Bad Request and the error message 9 | } else { 10 | next(err); // Pass other errors to the default error handler 11 | } 12 | }; 13 | 14 | export default joiErrorHandler; 15 | -------------------------------------------------------------------------------- /src-typescript/app/models/mongoose.user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | name: String, 6 | email: { 7 | type: String, 8 | unique: true, 9 | }, 10 | password: String, 11 | //add more fields 12 | }, 13 | { 14 | timestamps: true, 15 | } 16 | ); 17 | 18 | export default mongoose.model('User', UserSchema); 19 | -------------------------------------------------------------------------------- /src-typescript/app/models/sequelize.user.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, DataType, Model, Table } from 'sequelize-typescript'; 2 | 3 | @Table 4 | export default class User extends Model { 5 | @Column({ 6 | type: DataType.STRING, 7 | allowNull: false, 8 | }) 9 | name: string; 10 | 11 | @Column({ 12 | type: DataType.STRING, 13 | allowNull: false, 14 | unique: true, 15 | }) 16 | email: string; 17 | 18 | @Column({ 19 | type: DataType.STRING, 20 | allowNull: false, 21 | }) 22 | password: string; 23 | 24 | @Column({ 25 | type: DataType.STRING, 26 | allowNull: true, 27 | }) 28 | phone: string; 29 | 30 | @Column({ 31 | type: DataType.STRING, 32 | allowNull: true, 33 | }) 34 | address: string; 35 | 36 | //add more fields 37 | } 38 | -------------------------------------------------------------------------------- /src-typescript/app/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import RegisterUserController from '../apis/user/controllers/register.user.controller'; 2 | import express from 'express'; 3 | import LoginUserController from '../apis/user/controllers/login.user.controller'; 4 | 5 | export default (app: express.Application) => { 6 | RegisterUserController.post(app, '/api/v1/user/register/', []); 7 | LoginUserController.post(app, '/api/v1/user/login/', []); 8 | RegisterUserController.socketIO('hello'); 9 | }; 10 | -------------------------------------------------------------------------------- /src-typescript/app/utils/AsyncHandler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | const asyncHandler = (fn: any) => (req: Request, res: Response, next: NextFunction) => { 4 | Promise.resolve(fn(req, res, next)).catch(next); 5 | }; 6 | 7 | export default asyncHandler; 8 | -------------------------------------------------------------------------------- /src-typescript/app/utils/CronBuilder.ts: -------------------------------------------------------------------------------- 1 | import { CronMonth, CronWeekday } from '../enums/CronJob'; 2 | 3 | class CronBuilder { 4 | private readonly fields: { [key: string]: string }; 5 | 6 | constructor() { 7 | this.fields = { 8 | second: '*', 9 | minute: '*', 10 | hour: '*', 11 | dayOfMonth: '*', 12 | month: '*', 13 | dayOfWeek: '*', 14 | }; 15 | } 16 | 17 | // Entry point for every() 18 | public every(): Every { 19 | return new Every(); 20 | } 21 | 22 | /** 23 | * @description Helper method to set the field value in the cron pattern 24 | * @param field - Field name to be set 25 | * @param value - Value to be set 26 | * @protected 27 | */ 28 | protected setField(field: string, value: string): void { 29 | if (!['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'].includes(field)) { 30 | throw new Error('Invalid field name'); 31 | } 32 | this.fields[field] = value; 33 | } 34 | 35 | /** 36 | * @description Method to build the cron pattern 37 | * @private 38 | */ 39 | public build(): string { 40 | return [ 41 | this.fields.second, 42 | this.fields.minute, 43 | this.fields.hour, 44 | this.fields.dayOfMonth, 45 | this.fields.month, 46 | this.fields.dayOfWeek, 47 | ].join(' '); 48 | } 49 | } 50 | 51 | class Every extends CronBuilder { 52 | /** 53 | * @description Method to set the seconds in the cron pattern 54 | * If `second()` is called without a parameter, it assumes "every second" 55 | * @param second - Seconds 56 | */ 57 | public second(second?: number): CronBuilder { 58 | if (second === undefined) { 59 | this.setField('second', '*'); // Every second 60 | } else { 61 | if (second < 1 || second > 59) { 62 | throw new Error('Invalid value for seconds in Cron Pattern'); 63 | } 64 | this.setField('second', `*/${second}`); // Every N seconds 65 | } 66 | return this; 67 | } 68 | 69 | /** 70 | * @description Method to set the specific seconds in the cron pattern 71 | * @param seconds - Array of seconds 72 | */ 73 | public specificSeconds(seconds: number[]): CronBuilder { 74 | if (seconds.length === 0) { 75 | throw new Error('No seconds provided'); 76 | } 77 | 78 | if (seconds.some((second) => second < 0 || second > 59)) { 79 | throw new Error('Invalid value for seconds in Cron Pattern'); 80 | } 81 | const secondsString = seconds.join(','); 82 | this.setField('second', secondsString); 83 | return this; 84 | } 85 | 86 | /** 87 | * @description Method to set the seconds range in the cron pattern 88 | * @param start - Start value 89 | * @param end - End value 90 | */ 91 | public secondBetween(start: number, end: number): CronBuilder { 92 | if (start < 0 || start > 59 || end < 0 || end > 59) { 93 | throw new Error('Invalid value for seconds in Cron Pattern'); 94 | } 95 | this.setField('second', `${start}-${end}`); 96 | return this; 97 | } 98 | 99 | /** 100 | * @description Method to set the minutes in the cron pattern 101 | * If `minute()` is called without a parameter, it assumes "every minute" 102 | * @param minute - Minutes 103 | */ 104 | public minute(minute?: number): CronBuilder { 105 | if (minute === undefined) { 106 | this.setField('minute', '*'); // Every minute 107 | } else { 108 | if (minute < 0 || minute > 59) { 109 | throw new Error('Invalid value for minutes in Cron Pattern'); 110 | } 111 | this.setField('minute', `*/${minute}`); // Every N minutes 112 | } 113 | return this; 114 | } 115 | 116 | /** 117 | * @description Method to set the specific minutes in the cron pattern 118 | * @param minutes - Array of minutes 119 | */ 120 | public specificMinute(minutes: number[]): CronBuilder { 121 | if (minutes.length === 0) { 122 | throw new Error('No minutes provided'); 123 | } 124 | 125 | if (minutes.some((minute) => minute < 0 || minute > 59)) { 126 | throw new Error('Invalid value for minutes in Cron Pattern'); 127 | } 128 | const minutesString = minutes.join(','); 129 | this.setField('minute', minutesString); 130 | return this; 131 | } 132 | 133 | /** 134 | * @description Method to set the minutes range in the cron pattern 135 | * @param start - Start value 136 | * @param end - End value 137 | */ 138 | public minuteBetween(start: number, end: number): CronBuilder { 139 | if (start < 0 || start > 59 || end < 0 || end > 59) { 140 | throw new Error('Invalid value for minutes in Cron Pattern'); 141 | } 142 | this.setField('minute', `${start}-${end}`); 143 | return this; 144 | } 145 | 146 | /** 147 | * @description Method to set the hours in the cron pattern 148 | * If `hour()` is called without a parameter, it assumes "every hour" 149 | * @param hour - Hour 150 | */ 151 | public hour(hour?: number): CronBuilder { 152 | if (hour === undefined) { 153 | this.setField('hour', '*'); // Every hour 154 | } else { 155 | if (hour < 0 || hour > 23) { 156 | throw new Error('Invalid value for hours in Cron Pattern'); 157 | } 158 | this.setField('hour', `*/${hour}`); // Every N hours 159 | } 160 | return this; 161 | } 162 | 163 | /** 164 | * @description Method to set the specific hours in the cron pattern 165 | * @param hours - Array of hours 166 | */ 167 | public specificHour(hours: number[]): CronBuilder { 168 | if (hours.length === 0) { 169 | throw new Error('No hours provided'); 170 | } 171 | 172 | if (hours.some((hour) => hour < 0 || hour > 23)) { 173 | throw new Error('Invalid value for hours in Cron Pattern'); 174 | } 175 | const hoursString = hours.join(','); 176 | this.setField('hour', hoursString); 177 | return this; 178 | } 179 | 180 | /** 181 | * @description Method to set the hours range in the cron pattern 182 | * @param start - Start value 183 | * @param end - End value 184 | */ 185 | public hourBetween(start: number, end: number): CronBuilder { 186 | if (start < 0 || start > 23 || end < 0 || end > 23) { 187 | throw new Error('Invalid value for hours in Cron Pattern'); 188 | } 189 | this.setField('hour', `${start}-${end}`); 190 | return this; 191 | } 192 | 193 | /** 194 | * @description Method to set the day of the month in the cron pattern 195 | * If `dayOfMonth()` is called without a parameter, it assumes "every day of the month" 196 | * @param n - Number of days 197 | */ 198 | public dayOfMonth(n?: number): CronBuilder { 199 | if (n === undefined) { 200 | this.setField('dayOfMonth', '*'); // Every day of the month 201 | } else { 202 | if (n < 1 || n > 31) { 203 | throw new Error('Invalid value for day of the month in Cron Pattern'); 204 | } 205 | this.setField('dayOfMonth', `*/${n}`); // Every N days of the month 206 | } 207 | return this; 208 | } 209 | 210 | /** 211 | * @description Method to set the specific days of the month in the cron pattern 212 | * @param n - Array of days 213 | */ 214 | public specificDayOfMonth(n: number[]): CronBuilder { 215 | if (n.length === 0) { 216 | throw new Error('No days provided'); 217 | } 218 | 219 | if (n.some((day) => day < 1 || day > 31)) { 220 | throw new Error('Invalid value for days in Cron Pattern'); 221 | } 222 | const daysString = n.join(','); 223 | this.setField('dayOfMonth', daysString); 224 | return this; 225 | } 226 | 227 | /** 228 | * @description Method to set the days of the month range in the cron pattern 229 | * @param start - Start value 230 | * @param end - End value 231 | */ 232 | public dayOfMonthBetween(start: number, end: number): CronBuilder { 233 | if (start < 1 || start > 31 || end < 1 || end > 31) { 234 | throw new Error('Invalid value for days in Cron Pattern'); 235 | } 236 | this.setField('dayOfMonth', `${start}-${end}`); 237 | return this; 238 | } 239 | 240 | /** 241 | * @description Method to set the day of the week in the cron pattern 242 | * If `dayOfWeek()` is called without a parameter, it assumes "every day of the week" 243 | * @param day - Day of the week 244 | */ 245 | public dayOfWeek(day?: CronWeekday): CronBuilder { 246 | if (day === undefined) { 247 | this.setField('dayOfWeek', '*'); // Every day of the week 248 | } else { 249 | this.setField('dayOfWeek', `${day}`); 250 | } 251 | return this; 252 | } 253 | 254 | /** 255 | * @description Method to set the specific days of the week in the cron pattern 256 | * @param days - Array of days 257 | */ 258 | public specificDayOfWeek(days: CronWeekday[]): CronBuilder { 259 | if (days.length === 0) { 260 | throw new Error('No days provided'); 261 | } 262 | 263 | const daysString = days.map((day) => `${day}`).join(','); 264 | this.setField('dayOfWeek', daysString); 265 | return this; 266 | } 267 | 268 | /** 269 | * @description Method to set the month in the cron pattern 270 | * If `month()` is called without a parameter, it assumes "every month" 271 | * @param month - Month 272 | */ 273 | public month(month?: CronMonth): CronBuilder { 274 | if (month === undefined) { 275 | this.setField('month', '*'); // Every month 276 | } else { 277 | this.setField('month', `${month}`); 278 | } 279 | return this; 280 | } 281 | 282 | /** 283 | * @description Method to set the specific months in the cron pattern 284 | * @param months - Array of months 285 | */ 286 | public specificMonth(months: CronMonth[]): CronBuilder { 287 | if (months.length === 0) { 288 | throw new Error('No months provided'); 289 | } 290 | 291 | const monthsString = months.map((month) => `${month}`).join(','); 292 | this.setField('month', monthsString); 293 | return this; 294 | } 295 | 296 | /** 297 | * @description Method to set the months range in the cron pattern 298 | * @param start - Start value 299 | * @param end - End value 300 | */ 301 | public specificMonthBetween(start: CronMonth, end: CronMonth): CronBuilder { 302 | const startMonth = Object.values(CronMonth).indexOf(start); 303 | const endMonth = Object.values(CronMonth).indexOf(end); 304 | 305 | if (startMonth === -1 || endMonth === -1) { 306 | throw new Error('Invalid value for months in Cron Pattern'); 307 | } 308 | 309 | this.setField('month', `${start}-${end}`); 310 | return this; 311 | } 312 | } 313 | 314 | export default CronBuilder; 315 | -------------------------------------------------------------------------------- /src-typescript/app/utils/EncryptionUtil.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import * as bcrypt from 'bcrypt'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import { IAccessToken } from '../common/interfaces'; 5 | 6 | export default class EncryptionUtil { 7 | static async hashPassword(password: string, salt: number = 10) { 8 | return await bcrypt.hash(password, salt); 9 | } 10 | 11 | static async comparePassword(enteredPassword: string, dbPassword: string) { 12 | return await bcrypt.compare(enteredPassword, dbPassword); 13 | } 14 | 15 | static generateJwtTokens(data: any): IAccessToken { 16 | return { 17 | accessToken: jwt.sign(data, process.env.JWT_SECRET!, { 18 | expiresIn: '2 days', 19 | }), 20 | refreshToken: jwt.sign(data, process.env.JWT_SECRET!, { 21 | expiresIn: '10 days', 22 | }), 23 | }; 24 | } 25 | 26 | static verifyToken(token: string) { 27 | return jwt.verify(token, process.env.JWT_SECRET!); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src-typescript/app/utils/MasterController.ts: -------------------------------------------------------------------------------- 1 | import RequestBuilder, { PayloadType } from './RequestBuilder'; 2 | import { Request, RequestHandler, Response, Router } from 'express'; 3 | import asyncHandler from './AsyncHandler'; 4 | import SwaggerConfig, { ISwaggerDoc, SwaggerMethod } from '../../config/swaggerConfig'; 5 | import ResponseBuilder from './ResponseBuilder'; 6 | import { Server, Socket } from 'socket.io'; 7 | 8 | interface IJoiErrors { 9 | query?: string[]; 10 | param?: string[]; 11 | body?: string[]; 12 | } 13 | 14 | interface ISocketClient { 15 | event: string; 16 | masterController: MasterController; 17 | } 18 | 19 | interface ICronJob { 20 | cronPattern: string; 21 | masterController: MasterController; 22 | } 23 | 24 | /** 25 | * @class MasterController 26 | * @description This class is used to create a controller class 27 | * @summary extends this class to create a controller class for a route or socket event 28 | * and override the restController or socketController method 29 | * to write the controller logic for the route or socket event respectively 30 | */ 31 | class MasterController { 32 | // start socket requests snippet 33 | private static socketRequests: ISocketClient[] = []; 34 | 35 | /** 36 | * @method MasterController.getSocketRequests 37 | * @description This static method is used to retrieve all the socket requests instances for a controller class. 38 | * 39 | * @returns {ISocketClient[]} - Returns an array of ISocketClient objects, each representing a socket request instance 40 | */ 41 | static getSocketRequests(): ISocketClient[] { 42 | return this.socketRequests; 43 | } 44 | 45 | // end socket requests snippet 46 | 47 | // start cron jobs snippet 48 | private static cronRequests: ICronJob[] = []; 49 | 50 | /** 51 | * @method MasterController.getCronRequests 52 | * @description This static method is used to retrieve all the cron job instances for a controller class. 53 | * 54 | * @returns {ICronJob[]} - Returns an array of ICronJob objects, each representing a cron job instance 55 | */ 56 | static getCronRequests(): ICronJob[] { 57 | return this.cronRequests; 58 | } 59 | 60 | // end cron jobs snippet 61 | 62 | /** 63 | * @method MasterController.doc 64 | * @description This method is used to get the swagger doc for a controller class 65 | * 66 | * @example 67 | * { 68 | * tags: ['User'], 69 | * summary: 'Register User', 70 | * description: 'Register User Description', 71 | * } 72 | * @returns {Object} swagger doc for a controller class {@link ISwaggerDoc} 73 | */ 74 | static doc(): ISwaggerDoc | boolean { 75 | return { 76 | tags: [], 77 | summary: '', 78 | description: '', 79 | }; 80 | } 81 | 82 | /** 83 | * @method MasterController.validate 84 | * @static 85 | * @description Creates a new RequestBuilder object configured for validating request payloads. 86 | * @returns {RequestBuilder} - RequestBuilder object configured for validation. 87 | * @example 88 | * // Create a payload validator using RequestBuilder 89 | * const payloadValidator = RequestBuilder.validate(); 90 | * // Add validation rules to the payload validator 91 | * payloadValidator.addToBody( 92 | * Joi.object().keys({ 93 | * email: Joi.string().email().required(), 94 | * password: Joi.string().min(8).max(20).required(), 95 | * }) 96 | * ); 97 | * payloadValidator.addToQuery( 98 | * Joi.object().keys({ 99 | * limit: Joi.number().required(), 100 | * offset: Joi.number().required(), 101 | * }) 102 | * ); 103 | * payloadValidator.addToParams( 104 | * Joi.object().keys({ 105 | * id: Joi.number().required(), 106 | * }) 107 | * ); 108 | * // Return the configured payload validator for further usage 109 | * return payloadValidator; 110 | */ 111 | static validate(): RequestBuilder { 112 | return new RequestBuilder(); 113 | } 114 | 115 | /** 116 | * @method MasterController.restController 117 | * @description Handles the controller logic after validating the request payload. 118 | * @param params - Parameters from the request URL. 119 | * @param query - Query parameters from the request URL. 120 | * @param body - Body content from the request. 121 | * @param headers - Headers from the request. 122 | * @param allData - Contains all data including params, query, body, headers, and custom data from middlewares. 123 | * @protected This method is protected and can only be accessed by the child class. 124 | * @returns {Promise} Promise resolving to any value representing the response. 125 | */ 126 | async restController( 127 | params: P, 128 | query: Q, 129 | body: B, 130 | headers: any, 131 | allData: any 132 | ): Promise { 133 | // Controller logic goes here 134 | console.log(params, query, body, headers, allData); 135 | // Return a ResponseBuilder instance 136 | return new ResponseBuilder(200, null, 'Success'); 137 | } 138 | 139 | /** 140 | * @method MasterController.socketController 141 | * @description Handles the logic for socket events. 142 | * @param io - Instance of the Socket.IO server. 143 | * @param socket - Socket instance representing the client connection. 144 | * @param payload - Payload data received from the client. 145 | * @protected This method is protected and can only be accessed by the child class. 146 | * @returns {any} Returns any value, usually the response or processing result. 147 | */ 148 | socketController(io: Server, socket: Socket, payload: any): void { 149 | // Logic for handling socket events goes here 150 | console.log(io, socket, payload); 151 | } 152 | 153 | /** 154 | * @method MasterController.cronController 155 | * @description Handles the logic for cron jobs. 156 | */ 157 | cronController(): void { 158 | // Implement cron job logic here 159 | console.log('Cron job executed'); 160 | } 161 | 162 | /** 163 | * @method MasterController.joiValidator 164 | * @description Validates the request payload based on provided validation rules. 165 | * @param {any} params - Parameters from the request URL. 166 | * @param {any} query - Query parameters from the request URL. 167 | * @param {any} body - Body content from the request. 168 | * @param {RequestBuilder} validationRules - Validation rules obtained from the validate method 169 | * (Joi validation rules are written in the validate method). 170 | * @private This method is private and can only be accessed by the MasterController class. 171 | * @returns {IJoiErrors | null} Returns an object containing any validation errors found, or null 172 | * if there are no errors. 173 | */ 174 | private static joiValidator( 175 | params: any, 176 | query: any, 177 | body: any, 178 | validationRules: RequestBuilder 179 | ): IJoiErrors | null { 180 | // Check if there are no validation rules, return null (no validation needed) 181 | if (validationRules.get.length === 0) { 182 | return null; 183 | } 184 | 185 | // Object to store validation errors 186 | const joiErrors: IJoiErrors = { 187 | query: [], 188 | param: [], 189 | body: [], 190 | }; 191 | 192 | // Loop through each payload type and validate the corresponding data 193 | validationRules.payload.forEach((payload) => { 194 | if (payload.type === PayloadType.PARAMS) { 195 | // Validate params based on schema 196 | const schema = payload.schema; 197 | const { error } = schema.validate(params, { 198 | abortEarly: false, 199 | allowUnknown: true, 200 | }); 201 | if (error) { 202 | // Push validation error messages to the respective array 203 | joiErrors.param?.push(...error.details.map((err) => err.message)); 204 | } 205 | } else if (payload.type === PayloadType.QUERY) { 206 | // Validate query based on schema 207 | const schema = payload.schema; 208 | const { error } = schema.validate(query, { abortEarly: false, allowUnknown: true }); 209 | if (error) { 210 | // Push validation error messages to the respective array 211 | joiErrors.query?.push(...error.details.map((err) => err.message)); 212 | } 213 | } else if (payload.type === PayloadType.BODY) { 214 | // Validate body based on schema 215 | const schema = payload.schema; 216 | const { error } = schema.validate(body, { abortEarly: false, allowUnknown: true }); 217 | if (error) { 218 | // Push validation error messages to the respective array 219 | joiErrors.body?.push(...error.details.map((err) => err.message)); 220 | } 221 | } 222 | }); 223 | 224 | // Remove empty arrays from joiErrors 225 | if (joiErrors.query?.length === 0) delete joiErrors.query; 226 | if (joiErrors.param?.length === 0) delete joiErrors.param; 227 | if (joiErrors.body?.length === 0) delete joiErrors.body; 228 | 229 | // Return null if no errors, i.e. all arrays are removed above 230 | if (Object.keys(joiErrors).length === 0) return null; 231 | 232 | return joiErrors; 233 | } 234 | 235 | /** 236 | * @method MasterController.handler 237 | * @description Handles the request and response. 238 | * @private This method is private and can only be accessed by the MasterController class. 239 | * @returns {RequestHandler} Returns an Express RequestHandler function. 240 | */ 241 | private static handler(): RequestHandler { 242 | // Using 'self' to access the class within the async function scope 243 | const self = this; 244 | 245 | // Returns an async function serving as a RequestHandler for Express 246 | return asyncHandler(async (req: Request, res: Response) => { 247 | // Create a new instance of the current class 248 | const controller = new self(); 249 | 250 | // Combine all request data into a single object 251 | const allData = { ...req.params, ...req.query, ...req.body, ...req.headers, ...req }; 252 | 253 | // Retrieve validation rules using 'validate' method 254 | const validationRules = this.validate(); 255 | 256 | // Perform payload validation and capture any validation errors 257 | const joiErrors = this.joiValidator(req.params, req.query, req.body, validationRules); 258 | 259 | // If there are validation errors, respond with a 400 status and the error details 260 | if (joiErrors) { 261 | return res.status(400).json({ 262 | status: 400, 263 | message: 'Validation Error', 264 | data: null, 265 | errors: joiErrors, 266 | }); 267 | } 268 | 269 | // Invoke the 'restController' method to handle the request and get the response 270 | const { response } = await controller.restController( 271 | req.params, 272 | req.query, 273 | req.body, 274 | req.headers, 275 | allData 276 | ); 277 | 278 | // Respond with the status and data from 'restController' method 279 | res.status(response.status).json(response); 280 | }); 281 | } 282 | 283 | /** 284 | * @method MasterController.get 285 | * @description Registers a GET route for the controller class. 286 | * @param {Router} router - Router object. 287 | * @param {string} path - Path for the route. 288 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 289 | * @returns {Router} Router object with the registered GET route. 290 | */ 291 | static get(router: Router, path: string, middlewares: RequestHandler[]): Router { 292 | SwaggerConfig.recordApi(path, SwaggerMethod.GET, this); 293 | return router.get(path, ...middlewares, this.handler()); 294 | } 295 | 296 | /** 297 | * @method MasterController.post 298 | * @description Registers a POST route for the controller class. 299 | * @param {Router} router - Router object. 300 | * @param {string} path - Path for the route. 301 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 302 | * @returns {Router} Router object with the registered POST route. 303 | */ 304 | static post(router: Router, path: string, middlewares: RequestHandler[]): Router { 305 | SwaggerConfig.recordApi(path, SwaggerMethod.POST, this); 306 | return router.post(path, ...middlewares, this.handler()); 307 | } 308 | 309 | /** 310 | * @method MasterController.put 311 | * @description Registers a PUT route for the controller class. 312 | * @param {Router} router - Router object. 313 | * @param {string} path - Path for the route. 314 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 315 | * @returns {Router} Router object with the registered PUT route. 316 | */ 317 | static put(router: Router, path: string, middlewares: RequestHandler[]): Router { 318 | SwaggerConfig.recordApi(path, SwaggerMethod.PUT, this); 319 | return router.put(path, ...middlewares, this.handler()); 320 | } 321 | 322 | /** 323 | * @method MasterController.delete 324 | * @description Registers a DELETE route for the controller class. 325 | * @param {Router} router - Router object. 326 | * @param {string} path - Path for the route. 327 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 328 | * @returns {Router} Router object with the registered DELETE route. 329 | */ 330 | static delete(router: Router, path: string, middlewares: RequestHandler[]): Router { 331 | SwaggerConfig.recordApi(path, SwaggerMethod.DELETE, this); 332 | return router.delete(path, ...middlewares, this.handler()); 333 | } 334 | 335 | /** 336 | * @method MasterController.patch 337 | * @description Registers a PATCH route for the controller class. 338 | * @param {Router} router - Router object. 339 | * @param {string} path - Path for the route. 340 | * @param {RequestHandler[]} middlewares - Middlewares for the route. 341 | * @returns {Router} Router object with the registered PATCH route. 342 | */ 343 | static patch(router: Router, path: string, middlewares: RequestHandler[]): Router { 344 | SwaggerConfig.recordApi(path, SwaggerMethod.PATCH, this); 345 | return router.patch(path, ...middlewares, this.handler()); 346 | } 347 | 348 | /** 349 | * @method MasterController.socketIO 350 | * @description Registers a socket event for the controller class. 351 | * @param {string} event - Event name. 352 | */ 353 | static socketIO(event: string) { 354 | this.socketRequests.push({ event, masterController: new this() }); 355 | } 356 | 357 | /** 358 | * @method MasterController.cronJob 359 | * @description Registers a cron job for the controller class. 360 | * @param {string} cronPattern - Cron pattern for the job. 361 | */ 362 | static cronJob(cronPattern: string) { 363 | this.cronRequests.push({ cronPattern, masterController: new this() }); 364 | } 365 | } 366 | 367 | export default MasterController; 368 | -------------------------------------------------------------------------------- /src-typescript/app/utils/RequestBuilder.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export enum PayloadType { 4 | PARAMS, 5 | QUERY, 6 | BODY, 7 | } 8 | 9 | class RequestBuilder { 10 | payload: { type: PayloadType; schema: Joi.ObjectSchema }[]; 11 | 12 | constructor() { 13 | this.payload = []; 14 | } 15 | 16 | addToParams(payload: Joi.ObjectSchema) { 17 | this.payload.push({ type: PayloadType.PARAMS, schema: payload }); 18 | } 19 | 20 | addToQuery(payload: Joi.ObjectSchema) { 21 | this.payload.push({ type: PayloadType.QUERY, schema: payload }); 22 | } 23 | 24 | addToBody(payload: Joi.ObjectSchema) { 25 | this.payload.push({ type: PayloadType.BODY, schema: payload }); 26 | } 27 | 28 | get get() { 29 | return this.payload; 30 | } 31 | } 32 | 33 | export default RequestBuilder; 34 | -------------------------------------------------------------------------------- /src-typescript/app/utils/ResponseBuilder.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from '../enums/StatusCodes'; 2 | 3 | export default class ResponseBuilder { 4 | response: { status: StatusCodes; message: string; data: any }; 5 | 6 | constructor(status: StatusCodes, data: any, message: string) { 7 | this.response = { 8 | status, 9 | data, 10 | message, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src-typescript/config/cronConfig.ts: -------------------------------------------------------------------------------- 1 | import MasterController from '../app/utils/MasterController'; 2 | import asyncHandler from '../app/utils/AsyncHandler'; 3 | import { CronJob } from 'cron'; 4 | import * as fs from 'node:fs/promises'; 5 | import * as path from 'node:path'; 6 | 7 | class CronConfig { 8 | /** 9 | * @description Method to initialize the cron jobs 10 | * @param dir - The directory to search for cron jobs 11 | * @param loadCrons - lambda function to load the cron jobs from the directory 12 | */ 13 | static InitCronJobs = async (dir: string, loadCrons: (pathToCron: string) => void) => { 14 | const entries = await fs.readdir(dir, { withFileTypes: true }); 15 | 16 | for (const entry of entries) { 17 | const fullPath = path.join(dir, entry.name); 18 | 19 | if (entry.isDirectory()) { 20 | await CronConfig.InitCronJobs(fullPath, loadCrons); 21 | } else if ( 22 | entry.isFile() && 23 | (entry.name.endsWith('.cron.ts') || entry.name.endsWith('.cron.js')) 24 | ) { 25 | loadCrons(fullPath); 26 | } 27 | } 28 | }; 29 | 30 | /** 31 | * @description Method to start the cron jobs for the registered crons 32 | */ 33 | static startCronJobs = () => { 34 | MasterController.getCronRequests().forEach((client) => { 35 | asyncHandler( 36 | (async () => { 37 | const cron = new CronJob(client.cronPattern, () => { 38 | client.masterController.cronController(); 39 | }); 40 | cron.start(); 41 | })() 42 | ); 43 | }); 44 | }; 45 | } 46 | 47 | export default CronConfig; 48 | -------------------------------------------------------------------------------- /src-typescript/config/expressConfig.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import cors from 'cors'; 3 | import morgan from 'morgan'; 4 | import joiErrorHandler from '../app/handlers/JoiErrorHandler'; 5 | import customErrorHandler from '../app/handlers/CustomErrorHandler'; 6 | import * as fs from 'fs/promises'; 7 | import * as path from 'path'; 8 | // start swagger import 9 | import swaggerUI from 'swagger-ui-express'; 10 | import SwaggerConfig from './swaggerConfig'; 11 | // end swagger import 12 | 13 | const server = async () => { 14 | const app = express(); 15 | app.use(express.static('public')); 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: true })); 18 | app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); 19 | app.use( 20 | cors({ 21 | origin: '*', 22 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 23 | allowedHeaders: [ 24 | 'Content-Type', 25 | 'Authorization', 26 | 'X-Requested-With', 27 | 'Accept', 28 | 'Origin', 29 | 'Access-Control-Allow-Headers', 30 | 'Access-Control-Allow-Origin', 31 | 'Access-Control-Allow-Methods', 32 | 'Accept-Encoding', 33 | 'Accept-Language', 34 | 'Cache-Control', 35 | 'Connection', 36 | 'Content-Length', 37 | 'Host', 38 | 'Pragma', 39 | 'Referer', 40 | 'User-Agent', 41 | 'X-Forwarded-For', 42 | 'X-Forwarded-Proto', 43 | ], 44 | exposedHeaders: [ 45 | 'Content-Type', 46 | 'Authorization', 47 | 'X-Requested-With', 48 | 'Accept', 49 | 'Origin', 50 | 'Access-Control-Allow-Headers', 51 | 'Access-Control-Allow-Origin', 52 | 'Access-Control-Allow-Methods', 53 | 'Accept-Encoding', 54 | 'Accept-Language', 55 | 'Cache-Control', 56 | 'Connection', 57 | 'Content-Length', 58 | 'Host', 59 | 'Pragma', 60 | 'Referer', 61 | 'User-Agent', 62 | 'X-Forwarded-For', 63 | 'X-Forwarded-Proto', 64 | ], 65 | optionsSuccessStatus: 204, 66 | credentials: true, 67 | preflightContinue: false, 68 | }) 69 | ); 70 | const loadRouters = async (dir: string) => { 71 | //load all routers from dir and sub dir 72 | const entries = await fs.readdir(dir, { withFileTypes: true }); 73 | 74 | for (const entry of entries) { 75 | const fullPath = path.join(dir, entry.name); 76 | 77 | if (entry.isDirectory()) { 78 | //recursive call to sub dir 79 | await loadRouters(fullPath); 80 | } else if ( 81 | entry.isFile() && 82 | (entry.name.endsWith('.routes.ts') || entry.name.endsWith('.routes.js')) 83 | ) { 84 | const router = require(fullPath); 85 | //to support both default exports in commonjs and es6 86 | if (router.default) router.default(app); 87 | else router(app); 88 | } 89 | } 90 | }; 91 | // start swagger config 92 | SwaggerConfig.initSwagger({ 93 | title: 'Node Swagger API', 94 | description: 'Demonstrating how to describe a RESTful API with Swagger', 95 | version: '1.0.0', 96 | swaggerDocPath: path.join(__dirname, '../../swagger.json'), 97 | modifySwaggerDoc: false, 98 | }); 99 | // end swagger config 100 | await loadRouters(path.join(__dirname, '../app/routes')); 101 | app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(SwaggerConfig.getSwaggerDocument())); 102 | app.use( 103 | joiErrorHandler, 104 | customErrorHandler, 105 | (err: any, _req: Request, res: Response, _next: NextFunction) => { 106 | console.error(err); // Log the error for debugging 107 | return res.status(500).json({ error: 'Internal Server Error' }); // Respond with a 500 Internal Server Error 108 | } 109 | ); 110 | 111 | // no route found 112 | app.use((_req: Request, res: Response) => { 113 | return res.status(404).json({ error: 'Route Not Found' }); 114 | }); 115 | return app; 116 | }; 117 | 118 | export default server; 119 | -------------------------------------------------------------------------------- /src-typescript/config/mongooseConfig.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { ConnectOptions } from 'mongoose'; 2 | import * as process from 'process'; 3 | 4 | require('dotenv').config(); 5 | const mongooseOptions: ConnectOptions = { 6 | maxPoolSize: 10, 7 | serverSelectionTimeoutMS: 5000, 8 | socketTimeoutMS: 45000, 9 | }; 10 | 11 | export const mongooseConnect = async () => { 12 | try { 13 | await mongoose.connect(process.env.MONGO_URI!, mongooseOptions); 14 | console.log('\x1b[32m%s\x1b[0m', 'Database Connected successfully.'); 15 | } catch (err) { 16 | console.error('Unable to connect to the database:', err); 17 | throw err; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src-typescript/config/sequelizeConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | require('dotenv').config(); 4 | import { Op } from 'sequelize'; 5 | import { Sequelize, SequelizeOptions } from 'sequelize-typescript'; 6 | 7 | const sequelizeOptions: SequelizeOptions = { 8 | database: process.env.DB_NAME, 9 | username: process.env.DB_USER, 10 | password: process.env.DB_PASS, 11 | host: process.env.DB_HOST, 12 | port: Number(process.env.DB_PORT), 13 | dialect: 'postgres', 14 | dialectOptions: { 15 | ssl: false, 16 | }, 17 | models: [path.join(__dirname, '..', 'app', 'models')], 18 | logging: console.log, 19 | operatorsAliases: { 20 | $eq: Op.eq, 21 | $ne: Op.ne, 22 | $gte: Op.gte, 23 | $gt: Op.gt, 24 | $lte: Op.lte, 25 | $lt: Op.lt, 26 | $not: Op.not, 27 | $in: Op.in, 28 | $notIn: Op.notIn, 29 | $is: Op.is, 30 | $like: Op.like, 31 | $notLike: Op.notLike, 32 | $iLike: Op.iLike, 33 | $notILike: Op.notILike, 34 | $regexp: Op.regexp, 35 | $notRegexp: Op.notRegexp, 36 | $iRegexp: Op.iRegexp, 37 | $notIRegexp: Op.notIRegexp, 38 | $between: Op.between, 39 | $notBetween: Op.notBetween, 40 | $overlap: Op.overlap, 41 | $contains: Op.contains, 42 | $contained: Op.contained, 43 | $adjacent: Op.adjacent, 44 | $strictLeft: Op.strictLeft, 45 | $strictRight: Op.strictRight, 46 | $noExtendRight: Op.noExtendRight, 47 | $noExtendLeft: Op.noExtendLeft, 48 | $and: Op.and, 49 | $or: Op.or, 50 | $any: Op.any, 51 | $all: Op.all, 52 | $values: Op.values, 53 | $col: Op.col, 54 | }, 55 | pool: { 56 | max: 5, 57 | min: 0, 58 | acquire: 30000, 59 | idle: 10000, 60 | }, 61 | retry: { 62 | match: [ 63 | /ETIMEDOUT/, 64 | /EHOSTUNREACH/, 65 | /ECONNRESET/, 66 | /ECONNREFUSED/, 67 | /ETIMEDOUT/, 68 | /ESOCKETTIMEDOUT/, 69 | /EHOSTUNREACH/, 70 | /EPIPE/, 71 | /EAI_AGAIN/, 72 | /SequelizeConnectionError/, 73 | /SequelizeConnectionRefusedError/, 74 | /SequelizeHostNotFoundError/, 75 | /SequelizeHostNotReachableError/, 76 | /SequelizeInvalidConnectionError/, 77 | /SequelizeConnectionTimedOutError/, 78 | ], 79 | max: 3, 80 | }, 81 | }; 82 | 83 | export const sequelize = new Sequelize(sequelizeOptions); 84 | 85 | export const sequelizeConnect = async () => { 86 | try { 87 | await sequelize.authenticate(); 88 | console.log('\x1b[32m%s\x1b[0m', 'Database Connected successfully.'); 89 | await sequelize.sync({ alter: false }); 90 | console.log('\x1b[32m%s\x1b[0m', 'Database Synced successfully.'); 91 | } catch (err) { 92 | console.error('Unable to connect to the database:', err); 93 | throw err; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src-typescript/config/socketConfig.ts: -------------------------------------------------------------------------------- 1 | import { Server, Socket } from 'socket.io'; 2 | import http from 'http'; 3 | import MasterController from '../app/utils/MasterController'; 4 | import asyncHandler from '../app/utils/AsyncHandler'; 5 | 6 | class SocketConfig { 7 | /** 8 | * @description Method to initialize the socket io instance 9 | * @param server http server instance 10 | */ 11 | static init = (server: http.Server) => { 12 | return new Server(server, { 13 | /* Socket.IO options (if needed) */ 14 | cors: { 15 | origin: '*', 16 | methods: ['GET', 'POST'], 17 | }, 18 | }); 19 | }; 20 | 21 | /** 22 | * @description This method is used to handle the socket connection and listen for events 23 | * @param io socket io instance 24 | * @param socket socket instance 25 | */ 26 | static socketListener = (io: Server, socket: Socket) => { 27 | console.log('New client connected'); 28 | socket.on('disconnect', () => { 29 | console.log('Client disconnected'); 30 | }); 31 | 32 | MasterController.getSocketRequests().forEach((client) => { 33 | socket.on(client.event, (payload) => { 34 | asyncHandler( 35 | (async () => { 36 | client.masterController.socketController(io, socket, payload); 37 | })() 38 | ); 39 | }); 40 | }); 41 | }; 42 | } 43 | 44 | export default SocketConfig; 45 | -------------------------------------------------------------------------------- /src-typescript/config/swaggerConfig.ts: -------------------------------------------------------------------------------- 1 | import MasterController from '../app/utils/MasterController'; 2 | import RequestBuilder, { PayloadType } from '../app/utils/RequestBuilder'; 3 | import j2s from 'joi-to-swagger'; 4 | import * as fs from 'fs/promises'; 5 | 6 | export enum SwaggerMethod { 7 | GET = 'get', 8 | POST = 'post', 9 | PUT = 'put', 10 | DELETE = 'delete', 11 | PATCH = 'patch', 12 | } 13 | 14 | export interface ISwaggerDoc { 15 | tags: string[]; 16 | summary: string; 17 | description: string; 18 | } 19 | 20 | interface Schema { 21 | type?: string; 22 | required?: string[]; 23 | properties?: { [property: string]: Schema }; 24 | format?: string; 25 | minimum?: number; 26 | example?: any; 27 | additionalProperties?: boolean | Schema; 28 | } 29 | 30 | interface Parameter { 31 | name: string; 32 | in: string; 33 | required: boolean; 34 | type?: string; 35 | format?: string; 36 | schema?: Schema; 37 | } 38 | 39 | interface Response { 40 | description: string; 41 | schema?: Schema; 42 | } 43 | 44 | interface Method { 45 | tags: string[]; 46 | summary: string; 47 | description: string; 48 | operationId?: string; 49 | produces: string[]; 50 | parameters?: Parameter[]; 51 | responses: { [responseCode: string]: Response }; 52 | } 53 | 54 | interface Path { 55 | get?: Method; 56 | post?: Method; 57 | put?: Method; 58 | delete?: Method; 59 | patch?: Method; 60 | } 61 | 62 | interface Paths { 63 | [path: string]: Path; 64 | } 65 | 66 | interface SecurityDefinition { 67 | type: string; 68 | name: string; 69 | in: string; 70 | } 71 | 72 | interface SwaggerDocument { 73 | swagger: string; 74 | info: { 75 | version: string; 76 | title: string; 77 | description: string; 78 | }; 79 | schemes: string[]; 80 | consumes: string[]; 81 | produces: string[]; 82 | securityDefinitions: { [securityDefinition: string]: SecurityDefinition }; 83 | paths: Paths; 84 | } 85 | 86 | // exported because it is used in another repo express-master-controller which is connected through workflow 87 | export interface SwaggerConfigOptions { 88 | title: string; 89 | description: string; 90 | version: string; 91 | swaggerDocPath?: string; 92 | modifySwaggerDoc?: Boolean; 93 | } 94 | 95 | class SwaggerConfig { 96 | private static swaggerDocument: SwaggerDocument; 97 | private static swaggerPath: string; 98 | private static swaggerModify: Boolean | undefined; 99 | 100 | static initSwagger(options: SwaggerConfigOptions) { 101 | const { title, description, version, swaggerDocPath, modifySwaggerDoc } = options; 102 | if (swaggerDocPath) { 103 | this.swaggerPath = swaggerDocPath; 104 | this.swaggerModify = modifySwaggerDoc; 105 | this.swaggerDocument = require(swaggerDocPath); 106 | this.swaggerDocument.paths = {}; 107 | 108 | if (this.swaggerModify) { 109 | this.modifySwaggerDocument(); 110 | } 111 | } else { 112 | this.swaggerDocument = { 113 | swagger: '2.0', 114 | info: { 115 | title, 116 | description, 117 | version, 118 | }, 119 | schemes: ['http', 'https'], 120 | consumes: ['application/json'], 121 | produces: ['application/json'], 122 | securityDefinitions: { 123 | token: { 124 | type: 'apiKey', 125 | name: 'Authorization', 126 | in: 'header', 127 | }, 128 | }, 129 | paths: {}, 130 | }; 131 | } 132 | } 133 | 134 | static getSwaggerDocument() { 135 | return this.swaggerDocument; 136 | } 137 | 138 | private static swaggerDocsFromJoiSchema(validationRules: RequestBuilder) { 139 | const parameters: Parameter[] = []; 140 | 141 | validationRules.payload.forEach((payload) => { 142 | if (payload.type === PayloadType.PARAMS) { 143 | const schema = payload.schema; 144 | const { swagger } = j2s(schema); 145 | for (const key in swagger.properties) { 146 | const property = swagger.properties[key]; 147 | const parameter: Parameter = { 148 | name: key, 149 | in: 'path', 150 | required: swagger.required?.includes(key) ?? false, 151 | type: property.type, 152 | format: property.format, 153 | }; 154 | parameters.push(parameter); 155 | } 156 | } else if (payload.type === PayloadType.QUERY) { 157 | const schema = payload.schema; 158 | const { swagger } = j2s(schema); 159 | for (const key in swagger.properties) { 160 | const property = swagger.properties[key]; 161 | const parameter: Parameter = { 162 | name: key, 163 | in: 'query', 164 | required: swagger.required?.includes(key) ?? false, 165 | type: property.type, 166 | format: property.format, 167 | }; 168 | parameters.push(parameter); 169 | } 170 | } else if (payload.type === PayloadType.BODY) { 171 | const schema = payload.schema; 172 | const { swagger } = j2s(schema); 173 | const parameter: Parameter = { 174 | name: 'body', 175 | in: 'body', 176 | required: true, 177 | schema: swagger, 178 | }; 179 | parameters.push(parameter); 180 | } 181 | }); 182 | 183 | return parameters; 184 | } 185 | 186 | static recordApi(path: string, method: SwaggerMethod, currentRef: typeof MasterController) { 187 | if (currentRef.doc() === false) { 188 | return; 189 | } 190 | const key = path.replace(/:(\w+)/g, '{$&}').replace(/:/g, ''); 191 | const parameters = this.swaggerDocsFromJoiSchema(currentRef.validate()); 192 | const paths: Paths = this.swaggerDocument.paths; 193 | const pathObj: Path = paths[key] || {}; 194 | const methodObj: Method = pathObj[method] || { 195 | tags: [], 196 | summary: '', 197 | description: '', 198 | produces: ['application/json'], 199 | responses: this.exampleResponses(), 200 | }; 201 | 202 | methodObj.parameters = parameters; 203 | methodObj.tags = (currentRef.doc() as ISwaggerDoc).tags; 204 | methodObj.summary = (currentRef.doc() as ISwaggerDoc).summary; 205 | methodObj.description = (currentRef.doc() as ISwaggerDoc).description; 206 | methodObj.operationId = currentRef.name; 207 | pathObj[method] = methodObj; 208 | paths[key] = pathObj; 209 | this.swaggerDocument.paths = paths; 210 | 211 | if (this.swaggerModify) { 212 | this.modifySwaggerDocument(); 213 | } 214 | } 215 | 216 | private static modifySwaggerDocument() { 217 | fs.writeFile(this.swaggerPath, JSON.stringify(this.swaggerDocument, null, 2), { 218 | flag: 'w', 219 | }) 220 | .then(() => { 221 | console.log('Swagger document updated'); 222 | }) 223 | .catch((err) => { 224 | console.log('Error updating swagger document', err); 225 | }); 226 | } 227 | 228 | private static exampleResponses() { 229 | return { 230 | 200: { 231 | description: 'OK', 232 | schema: { 233 | type: 'object', 234 | properties: { 235 | status: { 236 | type: 'string', 237 | example: 'success', 238 | }, 239 | data: { 240 | type: 'object', 241 | }, 242 | message: { 243 | type: 'string', 244 | example: 'Success', 245 | }, 246 | }, 247 | }, 248 | }, 249 | 400: { 250 | description: 'Bad Request', 251 | schema: { 252 | type: 'object', 253 | properties: { 254 | status: { 255 | type: 'string', 256 | example: 'error', 257 | }, 258 | data: { 259 | type: 'object', 260 | }, 261 | message: { 262 | type: 'string', 263 | example: 'Bad Request', 264 | }, 265 | errors: { 266 | type: 'object', 267 | }, 268 | }, 269 | }, 270 | }, 271 | 404: { 272 | description: 'Not Found', 273 | schema: { 274 | type: 'object', 275 | properties: { 276 | status: { 277 | type: 'string', 278 | example: 'error', 279 | }, 280 | data: { 281 | type: 'object', 282 | }, 283 | message: { 284 | type: 'string', 285 | example: 'Not Found', 286 | }, 287 | }, 288 | }, 289 | }, 290 | 500: { 291 | description: 'Internal Server Error', 292 | schema: { 293 | type: 'object', 294 | properties: { 295 | status: { 296 | type: 'string', 297 | example: 'error', 298 | }, 299 | data: { 300 | type: 'object', 301 | }, 302 | message: { 303 | type: 'string', 304 | example: 'Internal Server Error', 305 | }, 306 | }, 307 | }, 308 | }, 309 | }; 310 | } 311 | } 312 | 313 | export default SwaggerConfig; 314 | -------------------------------------------------------------------------------- /src-typescript/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import serverConfig from './config/expressConfig'; 3 | import * as process from 'process'; 4 | import { mongooseConnect } from './config/mongooseConfig'; 5 | import { sequelizeConnect } from './config/sequelizeConfig'; 6 | import SocketConfig from './config/socketConfig'; 7 | import CronConfig from './config/cronConfig'; 8 | import * as path from 'node:path'; 9 | 10 | require('dotenv').config(); 11 | 12 | const port = process.env.PORT || 3000; 13 | (async () => { 14 | const app = await serverConfig(); 15 | 16 | // getting the dialect from .env file 17 | if (!process.env.DB_DIALECT) { 18 | throw new Error('DB_DIALECT not found in .env file'); 19 | } 20 | 21 | // start if dialect valid 22 | if ( 23 | ![ 24 | 'postgres', 25 | 'mysql', 26 | 'mariadb', 27 | 'sqlite', 28 | 'mssql', 29 | 'db2', 30 | 'snowflake', 31 | 'oracle', 32 | 'mongodb', 33 | ].includes(process.env.DB_DIALECT) 34 | ) { 35 | throw new Error('DB_DIALECT must be either postgres, mysql, mariadb, sqlite or mongodb'); 36 | } 37 | // end if dialect valid 38 | 39 | // Connect to the database 40 | // start if sequelize dialect check 41 | if ( 42 | ['postgres', 'mysql', 'mariadb', 'sqlite', 'mssql', 'db2', 'snowflake', 'oracle'].includes( 43 | process.env.DB_DIALECT 44 | ) 45 | ) { 46 | try { 47 | await sequelizeConnect(); 48 | } catch (err) { 49 | console.error('Unable to connect to the database:', err); 50 | throw err; 51 | } 52 | } 53 | // end if sequelize dialect check 54 | // start if mongoose dialect check 55 | else if (process.env.DB_DIALECT === 'mongodb') { 56 | try { 57 | await mongooseConnect(); 58 | } catch (err) { 59 | console.error('Unable to connect to the database:', err); 60 | throw err; 61 | } 62 | } 63 | // end if mongoose dialect check 64 | // Create an HTTP server instance 65 | const httpServer = http.createServer(app); 66 | 67 | // Initialize Socket.IO with the HTTP server 68 | const io = SocketConfig.init(httpServer); 69 | 70 | io.on('connection', (socket) => { 71 | SocketConfig.socketListener(io, socket); 72 | }); 73 | // End Initialize Socket.IO 74 | 75 | // Initialize Cron Jobs 76 | await CronConfig.InitCronJobs(path.join(__dirname, 'app/crons'), (pathToCron: string) => { 77 | // configurable import statement to load all the cron jobs before starting server 78 | // This lambda function is called for each cron job file found 79 | 80 | require(pathToCron); 81 | }); 82 | CronConfig.startCronJobs(); 83 | 84 | // End Initialize Cron Jobs 85 | 86 | // Start listening for HTTP requests 87 | httpServer.listen(port, () => { 88 | console.log(`Server is listening on port ${port}`); 89 | }); 90 | })(); 91 | -------------------------------------------------------------------------------- /swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Node Swagger API", 6 | "description": "Demonstrating how to describe a RESTful API with Swagger" 7 | }, 8 | "schemes": [ 9 | "http", 10 | "https" 11 | ], 12 | "consumes": [ 13 | "application/json" 14 | ], 15 | "produces": [ 16 | "application/json" 17 | ], 18 | "securityDefinitions": { 19 | "token": { 20 | "type": "apiKey", 21 | "name": "token", 22 | "in": "header" 23 | } 24 | }, 25 | "paths": { 26 | "/api/v1/user/register/{id}": { 27 | "get": { 28 | "tags": [ 29 | "User" 30 | ], 31 | "summary": "Register User", 32 | "description": "Register User", 33 | "produces": [ 34 | "application/json" 35 | ], 36 | "responses": { 37 | "200": { 38 | "description": "OK", 39 | "schema": { 40 | "type": "object", 41 | "properties": { 42 | "status": { 43 | "type": "string", 44 | "example": "success" 45 | }, 46 | "data": { 47 | "type": "object" 48 | }, 49 | "message": { 50 | "type": "string", 51 | "example": "Success" 52 | } 53 | } 54 | } 55 | }, 56 | "400": { 57 | "description": "Bad Request", 58 | "schema": { 59 | "type": "object", 60 | "properties": { 61 | "status": { 62 | "type": "string", 63 | "example": "error" 64 | }, 65 | "data": { 66 | "type": "object" 67 | }, 68 | "message": { 69 | "type": "string", 70 | "example": "Bad Request" 71 | }, 72 | "errors": { 73 | "type": "object" 74 | } 75 | } 76 | } 77 | }, 78 | "404": { 79 | "description": "Not Found", 80 | "schema": { 81 | "type": "object", 82 | "properties": { 83 | "status": { 84 | "type": "string", 85 | "example": "error" 86 | }, 87 | "data": { 88 | "type": "object" 89 | }, 90 | "message": { 91 | "type": "string", 92 | "example": "Not Found" 93 | } 94 | } 95 | } 96 | }, 97 | "500": { 98 | "description": "Internal Server Error", 99 | "schema": { 100 | "type": "object", 101 | "properties": { 102 | "status": { 103 | "type": "string", 104 | "example": "error" 105 | }, 106 | "data": { 107 | "type": "object" 108 | }, 109 | "message": { 110 | "type": "string", 111 | "example": "Internal Server Error" 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "parameters": [ 118 | { 119 | "name": "body", 120 | "in": "body", 121 | "required": true, 122 | "schema": { 123 | "type": "object", 124 | "properties": { 125 | "name": { 126 | "type": "string" 127 | }, 128 | "lastName": { 129 | "type": "string" 130 | }, 131 | "email": { 132 | "type": "string", 133 | "format": "email" 134 | }, 135 | "password": { 136 | "type": "string", 137 | "minLength": 8, 138 | "maxLength": 20 139 | } 140 | }, 141 | "required": [ 142 | "name", 143 | "lastName", 144 | "email", 145 | "password" 146 | ], 147 | "additionalProperties": false 148 | } 149 | }, 150 | { 151 | "name": "limit", 152 | "in": "query", 153 | "required": true, 154 | "type": "number", 155 | "format": "float" 156 | }, 157 | { 158 | "name": "offset", 159 | "in": "query", 160 | "required": true, 161 | "type": "number", 162 | "format": "float" 163 | }, 164 | { 165 | "name": "id", 166 | "in": "path", 167 | "required": true, 168 | "type": "number", 169 | "format": "float" 170 | } 171 | ], 172 | "operationId": "RegisterUserController" 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES6", 15 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | "experimentalDecorators": true 19 | /* Enable experimental support for legacy experimental decorators. */, 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "CommonJS", 31 | /* Specify what module code is generated. */ 32 | "rootDir": "./src-typescript", 33 | /* Specify the root folder within your source files. */ 34 | "moduleResolution": "node", 35 | /* Specify how TypeScript looks up a file from a given module specifier. */ 36 | "baseUrl": "./src-typescript", 37 | /* Specify the base directory to resolve non-relative module names. */ 38 | // "paths": { 39 | // "*": [ 40 | // "*", 41 | // "src/*" 42 | // ] 43 | // }, 44 | /* Specify a set of entries that re-map imports to additional lookup locations. */ 45 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 46 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 47 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 48 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 49 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 50 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 51 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 52 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 53 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 54 | "resolveJsonModule": true 55 | /* Enable importing .json files. */, 56 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 57 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 58 | 59 | /* JavaScript Support */ 60 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 61 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 62 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 63 | 64 | /* Emit */ 65 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 66 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 67 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 68 | "sourceMap": true 69 | /* Create source map files for emitted JavaScript files. */, 70 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 71 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 72 | "outDir": "./dist", 73 | /* Specify an output folder for all emitted files. */ 74 | "removeComments": true 75 | /* Disable emitting comments. */, 76 | // "noEmit": true, /* Disable emitting files from a compilation. */ 77 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 78 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 79 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 80 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 81 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 82 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 83 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 84 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 85 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 86 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 87 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 88 | "preserveConstEnums": true 89 | /* Disable erasing 'const enum' declarations in generated code. */, 90 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 91 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 92 | 93 | /* Interop Constraints */ 94 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 95 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 96 | "allowSyntheticDefaultImports": true, 97 | /* Allow 'import x from y' when a module doesn't have a default export. */ 98 | "esModuleInterop": true, 99 | /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 100 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 101 | "forceConsistentCasingInFileNames": true, 102 | /* Ensure that casing is correct in imports. */ 103 | 104 | /* Type Checking */ 105 | "strict": true, 106 | /* Enable all strict type-checking options. */ 107 | "noImplicitAny": true 108 | /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 109 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 110 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 111 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 112 | "strictPropertyInitialization": false 113 | /* Check for class properties that are declared but not set in the constructor. */, 114 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 115 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 116 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 117 | "noUnusedLocals": true 118 | /* Enable error reporting when local variables aren't read. */, 119 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 120 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 121 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 122 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 123 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 124 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 125 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 126 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 127 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 128 | 129 | /* Completeness */ 130 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 131 | "skipLibCheck": true 132 | /* Skip type checking all .d.ts files. */ 133 | } 134 | } 135 | --------------------------------------------------------------------------------