├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── babel.config.json ├── docs └── v1 │ ├── main.yaml │ ├── roles.yaml │ └── users.yaml ├── jest.config.json ├── package-lock.json ├── package.json ├── readme.md ├── src ├── @types │ └── index.d.ts ├── api │ ├── controllers │ │ ├── AuthController.ts │ │ ├── RoleController.ts │ │ └── UserController.ts │ ├── middlewares │ │ ├── auth │ │ │ └── index.ts │ │ ├── handlers │ │ │ └── error.ts │ │ ├── morgan │ │ │ └── index.ts │ │ └── validator │ │ │ ├── index.ts │ │ │ └── requirements │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ ├── roles.ts │ │ │ └── users.ts │ ├── models │ │ ├── Role.ts │ │ └── user.ts │ ├── repositories │ │ ├── RoleRepository.ts │ │ ├── UserRepository.ts │ │ └── __test__ │ │ │ ├── RoleRepository.test.ts │ │ │ ├── UserRepository.test.ts │ │ │ └── mockResource.ts │ ├── routes │ │ └── v1 │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ ├── roles.ts │ │ │ └── users.ts │ ├── services │ │ ├── AuthService.ts │ │ ├── RoleService.ts │ │ ├── UserService.ts │ │ └── __test__ │ │ │ ├── AuthService.test.ts │ │ │ ├── RoleService.test.ts │ │ │ ├── UserService.test.ts │ │ │ └── mockResource.ts │ └── types │ │ ├── auth.ts │ │ ├── role.ts │ │ └── user.ts ├── config │ └── appConfig.ts ├── constants │ └── index.ts ├── database │ ├── config.ts │ └── sync.ts ├── index.ts ├── server.ts └── utils │ ├── helpers │ └── index.ts │ ├── jwt │ └── index.ts │ ├── logger │ └── index.ts │ └── swagger │ └── index.ts ├── tea.yaml └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | APP_NAME=my-project 3 | SERVER=development 4 | PORT=3001 5 | SECRET=j!89nKO5as&Js 6 | API_VERSION=v1 7 | 8 | DB_HOST=localhost 9 | DB_DATABASE=test2 10 | DB_USERNAME=postgres 11 | DB_PASSWORD=postgres 12 | DB_PORT=5432 13 | DB_DIALECT=postgres 14 | DB_TIMEZONE=Asia/Jakarta 15 | DB_LOG=true 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "rules": { 12 | "no-console": 0, 13 | "no-async-promise-executor": 0, 14 | "prettier/prettier": 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | logs 5 | coverage 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | coverage 5 | logs 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { "node": "current" } 7 | } 8 | ], 9 | "@babel/preset-typescript" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/v1/main.yaml: -------------------------------------------------------------------------------- 1 | /login: 2 | post: 3 | summary: Login 4 | description: Login 5 | tags: 6 | - main 7 | requestBody: 8 | content: 9 | 'application/json': 10 | schema: 11 | properties: 12 | email: 13 | type: string 14 | format: email 15 | password: 16 | type: string 17 | required: 18 | - email 19 | - password 20 | example: 21 | email: 'admin@mail.com' 22 | password: 'admin' 23 | required: true 24 | responses: 25 | 200: 26 | $ref: '#/components/responses/200' 27 | 400: 28 | $ref: '#/components/responses/400' 29 | 401: 30 | $ref: '#/components/responses/401' 31 | 403: 32 | $ref: '#/components/responses/403' 33 | 422: 34 | $ref: '#/components/responses/422' 35 | /signup: 36 | post: 37 | summary: Signup 38 | description: Signup 39 | tags: 40 | - main 41 | requestBody: 42 | content: 43 | 'application/json': 44 | schema: 45 | properties: 46 | email: 47 | type: string 48 | format: email 49 | password: 50 | type: string 51 | firstName: 52 | type: string 53 | lastName: 54 | type: string 55 | required: 56 | - email 57 | - password 58 | - firstName 59 | example: 60 | email: 'user@mail.com' 61 | password: 'user' 62 | firstName: 'John' 63 | lastName: 'Doe' 64 | required: true 65 | responses: 66 | 200: 67 | $ref: '#/components/responses/200' 68 | 400: 69 | $ref: '#/components/responses/400' 70 | 401: 71 | $ref: '#/components/responses/401' 72 | 403: 73 | $ref: '#/components/responses/403' 74 | 422: 75 | $ref: '#/components/responses/422' 76 | -------------------------------------------------------------------------------- /docs/v1/roles.yaml: -------------------------------------------------------------------------------- 1 | /roles: 2 | post: 3 | summary: Create role 4 | description: Create role 5 | tags: 6 | - roles 7 | security: 8 | - bearerAuth: [] 9 | requestBody: 10 | content: 11 | 'application/json': 12 | schema: 13 | properties: 14 | name: 15 | type: string 16 | description: 17 | type: string 18 | required: 19 | - name 20 | example: 21 | name: 'Admin' 22 | description: 'Admin All User' 23 | required: true 24 | responses: 25 | 200: 26 | $ref: '#/components/responses/200' 27 | 400: 28 | $ref: '#/components/responses/400' 29 | 401: 30 | $ref: '#/components/responses/401' 31 | 403: 32 | $ref: '#/components/responses/403' 33 | 422: 34 | $ref: '#/components/responses/422' 35 | get: 36 | summary: Get roles 37 | description: Get roles 38 | tags: 39 | - roles 40 | security: 41 | - bearerAuth: [] 42 | responses: 43 | 200: 44 | $ref: '#/components/responses/200' 45 | 400: 46 | $ref: '#/components/responses/400' 47 | 401: 48 | $ref: '#/components/responses/401' 49 | 403: 50 | $ref: '#/components/responses/403' 51 | 422: 52 | $ref: '#/components/responses/422' 53 | -------------------------------------------------------------------------------- /docs/v1/users.yaml: -------------------------------------------------------------------------------- 1 | /users: 2 | post: 3 | summary: Create user 4 | description: Create user 5 | tags: 6 | - users 7 | security: 8 | - bearerAuth: [] 9 | requestBody: 10 | content: 11 | 'application/json': 12 | schema: 13 | properties: 14 | email: 15 | type: string 16 | format: email 17 | password: 18 | type: string 19 | firstName: 20 | type: string 21 | lastName: 22 | type: string 23 | roleId: 24 | type: number 25 | required: 26 | - email 27 | - password 28 | - firstName 29 | example: 30 | email: 'user@mail.com' 31 | password: 'user' 32 | firstName: 'John' 33 | lastName: 'Doe' 34 | roleId: 1 35 | required: true 36 | responses: 37 | 200: 38 | $ref: '#/components/responses/200' 39 | 400: 40 | $ref: '#/components/responses/400' 41 | 401: 42 | $ref: '#/components/responses/401' 43 | 403: 44 | $ref: '#/components/responses/403' 45 | 422: 46 | $ref: '#/components/responses/422' 47 | get: 48 | summary: Get users 49 | description: Get users 50 | tags: 51 | - users 52 | security: 53 | - bearerAuth: [] 54 | responses: 55 | 200: 56 | $ref: '#/components/responses/200' 57 | 400: 58 | $ref: '#/components/responses/400' 59 | 401: 60 | $ref: '#/components/responses/401' 61 | 403: 62 | $ref: '#/components/responses/403' 63 | 422: 64 | $ref: '#/components/responses/422' 65 | /users/{id}: 66 | get: 67 | summary: Get user detail 68 | description: Get user detail 69 | tags: 70 | - users 71 | security: 72 | - bearerAuth: [] 73 | parameters: 74 | - in: path 75 | name: id 76 | schema: 77 | type: number 78 | required: true 79 | responses: 80 | 200: 81 | $ref: '#/components/responses/200' 82 | 400: 83 | $ref: '#/components/responses/400' 84 | 401: 85 | $ref: '#/components/responses/401' 86 | 403: 87 | $ref: '#/components/responses/403' 88 | 422: 89 | $ref: '#/components/responses/422' 90 | put: 91 | summary: Update user 92 | description: Update user 93 | tags: 94 | - users 95 | security: 96 | - bearerAuth: [] 97 | parameters: 98 | - in: path 99 | name: id 100 | schema: 101 | type: number 102 | required: true 103 | requestBody: 104 | content: 105 | 'application/json': 106 | schema: 107 | properties: 108 | firstName: 109 | type: string 110 | lastName: 111 | type: string 112 | roleId: 113 | type: number 114 | required: 115 | - firstName 116 | example: 117 | firstName: 'John' 118 | lastName: 'Doe' 119 | roleId: 1 120 | required: true 121 | responses: 122 | 200: 123 | $ref: '#/components/responses/200' 124 | 400: 125 | $ref: '#/components/responses/400' 126 | 401: 127 | $ref: '#/components/responses/401' 128 | 403: 129 | $ref: '#/components/responses/403' 130 | 422: 131 | $ref: '#/components/responses/422' 132 | delete: 133 | summary: Delete user 134 | description: Delete user 135 | tags: 136 | - users 137 | security: 138 | - bearerAuth: [] 139 | parameters: 140 | - in: path 141 | name: id 142 | schema: 143 | type: number 144 | required: true 145 | responses: 146 | 200: 147 | $ref: '#/components/responses/200' 148 | 400: 149 | $ref: '#/components/responses/400' 150 | 401: 151 | $ref: '#/components/responses/401' 152 | 403: 153 | $ref: '#/components/responses/403' 154 | 422: 155 | $ref: '#/components/responses/422' 156 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "project-structure-api-test", 3 | "verbose": true, 4 | "roots": ["src/"], 5 | "collectCoverage": true, 6 | "collectCoverageFrom": [ 7 | "**/src/api/models/**", 8 | "**/src/api/repositories/**", 9 | "**/src/api/services/**", 10 | "!**/src/api/repositories/__test__/**", 11 | "!**/src/api/services/__test__/**" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-structure-api", 3 | "version": "2.0.1", 4 | "description": "Project structure for API", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "start": "node .", 9 | "start-watch": "nodemon --delay 5000ms --exec npm run start", 10 | "build": "tsc", 11 | "build-watch": "tsc -w", 12 | "lint": "eslint . --ext .ts", 13 | "prettier": "prettier --config .prettierrc --write .", 14 | "test": "jest", 15 | "test:noCoverage": "jest --collectCoverage=false", 16 | "sync-db": "node dist/database/sync" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/arifintahu/project-structure-api.git" 21 | }, 22 | "keywords": [ 23 | "nodejs", 24 | "typescript", 25 | "boilerplate", 26 | "project" 27 | ], 28 | "author": { 29 | "name": "Miftahul Arifin", 30 | "email": "miftahul97@gmail.com", 31 | "url": "https://github.com/arifintahu" 32 | }, 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/arifintahu/project-structure-api/issues" 36 | }, 37 | "homepage": "https://github.com/arifintahu/project-structure-api#readme", 38 | "dependencies": { 39 | "bcrypt": "^5.0.1", 40 | "compression": "^1.7.4", 41 | "cors": "^2.8.5", 42 | "dotenv": "^10.0.0", 43 | "express": "^4.17.1", 44 | "express-validator": "^6.14.2", 45 | "jsonwebtoken": "^8.5.1", 46 | "morgan": "^1.10.0", 47 | "pg": "^8.7.1", 48 | "pg-hstore": "^2.3.4", 49 | "sequelize": "^6.6.5", 50 | "swagger-jsdoc": "^6.1.0", 51 | "swagger-ui-express": "^4.1.6", 52 | "winston": "^3.3.3" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.18.13", 56 | "@babel/preset-env": "^7.18.10", 57 | "@babel/preset-typescript": "^7.18.6", 58 | "@types/bcrypt": "^5.0.0", 59 | "@types/compression": "^1.7.2", 60 | "@types/cors": "^2.8.12", 61 | "@types/express": "^4.17.13", 62 | "@types/jest": "^29.0.0", 63 | "@types/jsonwebtoken": "^8.5.5", 64 | "@types/morgan": "^1.9.3", 65 | "@types/node": "^16.10.1", 66 | "@types/sequelize": "^4.28.10", 67 | "@types/swagger-jsdoc": "^6.0.1", 68 | "@types/swagger-ui-express": "^4.1.3", 69 | "@typescript-eslint/eslint-plugin": "^4.32.0", 70 | "@typescript-eslint/parser": "^4.32.0", 71 | "babel-jest": "^29.0.1", 72 | "eslint": "^7.32.0", 73 | "eslint-config-prettier": "^8.3.0", 74 | "eslint-plugin-prettier": "^4.0.0", 75 | "husky": "^7.0.2", 76 | "jest": "^29.0.1", 77 | "lint-staged": "^11.1.2", 78 | "nodemon": "^2.0.13", 79 | "prettier": "^2.4.1", 80 | "ts-node": "^10.2.1", 81 | "typescript": "^4.4.3" 82 | }, 83 | "lint-staged": { 84 | "*.+(ts)": "eslint --fix", 85 | "*.+(ts|json|md)": "prettier --config .prettierrc --write ." 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Stargazers][stars-shield]][stars-url] 6 | [![Issues][issues-shield]][issues-url] 7 | 8 |
9 |
10 |

Project Structure API

11 | 12 |

13 | Complete project template for building RESTful API with Typescript 14 |
15 |
16 | Report Bug 17 | · 18 | Request Feature 19 |

20 |
21 | 22 | 23 |
24 | Table of Contents 25 |
    26 |
  1. 27 | About The Project 28 | 31 |
  2. 32 |
  3. 33 | Getting Started 34 | 38 |
  4. 39 |
  5. Usage
  6. 40 |
  7. Contributing
  8. 41 |
42 |
43 | 44 | 45 | 46 | ## About The Project 47 | 48 | Building project with standardized structure could save much our time. We could focus on business process without considering too much on project structure. On the other hand, a good project structure should be clean, nice refactored, and easy to maintain. 49 | 50 | Here's why: 51 | 52 | - Your time should be focused on creating something amazing. A project that solves a problem and helps others. 53 | - You shouldn't be doing the same tasks over and over like structuring project 54 | - You should implement dry principles to the rest of your life 55 | 56 | Of course, no one template will serve all projects since your needs may be different. So I'll be adding more in the near future. You may also suggest changes by forking this repo and creating a pull request or opening an issue. Thanks to all the people have contributed to expanding this project template! 57 | 58 |

(back to top)

59 | 60 | ### Built With 61 | 62 | This project structure is built using 63 | 64 | - [Express.js](https://expressjs.com/) 65 | - [Sequelize](https://sequelize.org/) 66 | - [Swagger](https://swagger.io/) 67 | - [Typescript](https://www.typescriptlang.org/) 68 | - [JsonWebToken](https://www.npmjs.com/package/jsonwebtoken) 69 | - [Postgresql](https://www.postgresql.org/) 70 | - [Jest](https://jestjs.io/) 71 | 72 |

(back to top)

73 | 74 | ### Features 75 | 76 | - Everything is modular and unit testable 77 | - Typescript everything 78 | - Project API structures with routes, controllers, models, repositories, middlewares, and services 79 | - Centralized configuration input validator 80 | 81 | ### Folder Structure 82 | 83 | ``` 84 | ├── .husky/ # Pre-commit config for lint staged 85 | ├── docs/ # Swagger API docs 86 | ├── src/ # All application source 87 | ├──── @types/ # Type definition for modules 88 | | 89 | ├──── api/ 90 | ├────── controllers/ # Define all controllers 91 | ├────── middlewares/ # Define all middlewares 92 | ├────── models/ # Define all sequelize models 93 | ├────── repositories/ # Define all repositories 94 | ├────── routes/ 95 | ├──────── v1/ # Define all v1 routes 96 | ├────── services/ # Define all services 97 | ├────── types/ # Define all input types 98 | | 99 | ├──── config/ 100 | ├────── appConfig.ts # Define app configuration 101 | | 102 | ├──── constants/ # Define all constants 103 | ├──── database/ # Define database connection and sync tables 104 | ├──── utils/ # Define reusable libs 105 | ├──── server.ts # Create express config 106 | ├──── index.ts # ENTRYPOINT - Start server 107 | | 108 | └── ... 109 | ``` 110 | 111 | 112 | 113 | ## Getting Started 114 | 115 | To start project, just clone this repo or [Use this template](https://github.com/arifintahu/project-structure-api/generate) 116 | 117 | ### Prerequisites 118 | 119 | Before installation, make sure you have the following prerequisites 120 | 121 | - NPM 122 | ```sh 123 | npm install npm@latest -g 124 | ``` 125 | - Postgresql server 126 | 127 | ### Installation 128 | 129 | 1. Clone the repo or simply select [use this template](https://github.com/arifintahu/project-structure-api/generate) 130 | ```sh 131 | git clone https://github.com/arifintahu/project-structure-api.git 132 | ``` 133 | 2. Install NPM packages 134 | ```sh 135 | npm ci 136 | ``` 137 | 3. Create `.env` file in main directory 138 | 4. Copy and customize envs from `.env.example` 139 | 5. Test and build the project 140 | ```sh 141 | npm run build 142 | ``` 143 | 6. Sync database tables 144 | ```sh 145 | npm run sync-db 146 | ``` 147 | 7. Run the server 148 | ```sh 149 | npm run start 150 | ``` 151 | 8. Access swagger docs from `localhost:3001/docs/v1` 152 | 153 |

(back to top)

154 | 155 | 156 | 157 | ## Contributing 158 | 159 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 160 | 161 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 162 | Don't forget to give the project a star! Thanks again! 163 | 164 | 1. Fork the Project 165 | 2. Create your Feature Branch (`git checkout -b feature/feature-name`) 166 | 3. Commit your Changes (`git commit -m 'Add some feature-name'`) 167 | 4. Push to the Branch (`git push origin feature/feature-name`) 168 | 5. Open a Pull Request 169 | 170 |

(back to top)

171 | 172 | 173 | 174 | 175 | [contributors-shield]: https://img.shields.io/github/contributors/arifintahu/project-structure-api.svg?style=for-the-badge 176 | [contributors-url]: https://github.com/arifintahu/project-structure-api/graphs/contributors 177 | [forks-shield]: https://img.shields.io/github/forks/arifintahu/project-structure-api.svg?style=for-the-badge 178 | [forks-url]: https://github.com/arifintahu/project-structure-api/network/members 179 | [stars-shield]: https://img.shields.io/github/stars/arifintahu/project-structure-api.svg?style=for-the-badge 180 | [stars-url]: https://github.com/arifintahu/project-structure-api/stargazers 181 | [issues-shield]: https://img.shields.io/github/issues/arifintahu/project-structure-api.svg?style=for-the-badge 182 | [issues-url]: https://github.com/arifintahu/project-structure-api/issues 183 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'express'; 2 | import { UserOutput } from '../api/models/User'; 3 | 4 | declare global { 5 | namespace Express { 6 | interface Request { 7 | userdata: UserOutput; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/api/controllers/AuthController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import AuthService from '../services/AuthService'; 3 | import { LoginType, SignUpType } from '../types/auth'; 4 | 5 | class AuthController { 6 | async login( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise { 11 | try { 12 | const payload: LoginType = req.body; 13 | const token = await AuthService.login(payload); 14 | res.status(200).send({ 15 | message: 'Logged in successfully', 16 | data: token 17 | }); 18 | } catch (error) { 19 | next(error); 20 | } 21 | } 22 | 23 | async signUp( 24 | req: Request, 25 | res: Response, 26 | next: NextFunction 27 | ): Promise { 28 | try { 29 | const payload: SignUpType = req.body; 30 | await AuthService.signUp(payload); 31 | res.status(200).send({ 32 | message: 'Signed up successfully' 33 | }); 34 | } catch (error) { 35 | next(error); 36 | } 37 | } 38 | } 39 | 40 | export default new AuthController(); 41 | -------------------------------------------------------------------------------- /src/api/controllers/RoleController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import RoleService from '../services/RoleService'; 3 | import { CreateRoleType } from '../types/role'; 4 | 5 | class RoleController { 6 | async createRole( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise { 11 | try { 12 | const payload: CreateRoleType = req.body; 13 | const role = await RoleService.createRole(payload); 14 | res.status(200).send({ 15 | message: 'Role created successfully', 16 | data: role 17 | }); 18 | } catch (error) { 19 | next(error); 20 | } 21 | } 22 | 23 | async getRoles( 24 | req: Request, 25 | res: Response, 26 | next: NextFunction 27 | ): Promise { 28 | try { 29 | const roles = await RoleService.getRoles(); 30 | res.status(200).send({ 31 | message: 'Roles fetched successfully', 32 | data: roles 33 | }); 34 | } catch (error) { 35 | next(error); 36 | } 37 | } 38 | } 39 | 40 | export default new RoleController(); 41 | -------------------------------------------------------------------------------- /src/api/controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import UserService from '../services/UserService'; 3 | import { CreateUserType, UpdateUserType } from '../types/user'; 4 | 5 | class UserController { 6 | async createUser( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise { 11 | try { 12 | const payload: CreateUserType = req.body; 13 | const user = await UserService.createUser(payload); 14 | res.status(200).send({ 15 | message: 'User created successfully', 16 | data: user 17 | }); 18 | } catch (error) { 19 | next(error); 20 | } 21 | } 22 | 23 | async getUsers( 24 | req: Request, 25 | res: Response, 26 | next: NextFunction 27 | ): Promise { 28 | try { 29 | const user = await UserService.getUsers(); 30 | res.status(200).send({ 31 | message: 'Users fetched successfully', 32 | data: user 33 | }); 34 | } catch (error) { 35 | next(error); 36 | } 37 | } 38 | 39 | async getUserDetail( 40 | req: Request, 41 | res: Response, 42 | next: NextFunction 43 | ): Promise { 44 | try { 45 | const userId = Number(req.params.id); 46 | const user = await UserService.getUserDetail(userId); 47 | res.status(200).send({ 48 | message: 'User details fetched successfully', 49 | data: user 50 | }); 51 | } catch (error) { 52 | next(error); 53 | } 54 | } 55 | 56 | async updateUser( 57 | req: Request, 58 | res: Response, 59 | next: NextFunction 60 | ): Promise { 61 | try { 62 | const userId = Number(req.params.id); 63 | const payload: UpdateUserType = req.body; 64 | await UserService.updateUser(userId, payload); 65 | res.status(200).send({ 66 | message: 'User updated successfully' 67 | }); 68 | } catch (error) { 69 | next(error); 70 | } 71 | } 72 | 73 | async deleteUser( 74 | req: Request, 75 | res: Response, 76 | next: NextFunction 77 | ): Promise { 78 | try { 79 | const userId = Number(req.params.id); 80 | await UserService.deleteUser(userId); 81 | res.status(200).send({ 82 | message: 'User deleted successfully' 83 | }); 84 | } catch (error) { 85 | next(error); 86 | } 87 | } 88 | } 89 | 90 | export default new UserController(); 91 | -------------------------------------------------------------------------------- /src/api/middlewares/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import JWT from '../../../utils/jwt'; 3 | import UserRepository from '../../repositories/UserRepository'; 4 | 5 | class Auth { 6 | async authenticate( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise { 11 | const authorization = String(req.headers.authorization); 12 | if (!authorization || !authorization.includes('Bearer')) { 13 | res.sendStatus(401); 14 | return; 15 | } 16 | const token = authorization?.slice(7); 17 | const payload = await JWT.verifyToken(token); 18 | 19 | if (!payload) { 20 | res.sendStatus(401); 21 | return; 22 | } 23 | 24 | const userId: number = payload.id; 25 | const userdata = await UserRepository.getUserDetail(userId); 26 | 27 | if (!userdata) { 28 | res.sendStatus(401); 29 | return; 30 | } 31 | req.userdata = userdata; 32 | 33 | next(); 34 | } 35 | 36 | checkRoles(...roles: string[]) { 37 | return async (req: Request, res: Response, next: NextFunction) => { 38 | const userdata = req.userdata; 39 | 40 | const roleUser = userdata.role?.slug; 41 | if (!roleUser) { 42 | res.sendStatus(403); 43 | return; 44 | } 45 | 46 | const isRoleValid = roles.includes(roleUser); 47 | if (!isRoleValid) { 48 | res.sendStatus(403); 49 | return; 50 | } 51 | next(); 52 | }; 53 | } 54 | } 55 | 56 | export default new Auth(); 57 | -------------------------------------------------------------------------------- /src/api/middlewares/handlers/error.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import AppConfig from '../../../config/appConfig'; 3 | import Logger from '../../../utils/logger'; 4 | 5 | type ResponseType = { 6 | message?: string; 7 | }; 8 | 9 | function errorHandler( 10 | err: Error, 11 | req: Request, 12 | res: Response, 13 | next: NextFunction 14 | ) { 15 | const response: ResponseType = {}; 16 | if (err.message) { 17 | const logs = { 18 | type: err.name, 19 | message: err.message, 20 | method: req.method, 21 | path: req.path, 22 | params: req.route.path, 23 | body: req.body, 24 | query: req.query, 25 | stack: err.stack 26 | }; 27 | Logger.error(JSON.stringify(logs)); 28 | response.message = AppConfig.app.isDevelopment 29 | ? err.message 30 | : 'Something wrong!'; 31 | } 32 | 33 | res.status(422).send(response); 34 | } 35 | 36 | export default errorHandler; 37 | -------------------------------------------------------------------------------- /src/api/middlewares/morgan/index.ts: -------------------------------------------------------------------------------- 1 | import * as morgan from 'morgan'; 2 | import { StreamOptions } from 'morgan'; 3 | 4 | import Logger from '../../../utils/logger'; 5 | 6 | // Override the stream method by telling 7 | // Morgan to use our custom logger instead of the console.log. 8 | const stream: StreamOptions = { 9 | // Use the http severity 10 | write: (message) => Logger.http(message) 11 | }; 12 | 13 | // Skip all the Morgan http log if the 14 | // application is not running in development mode. 15 | // This method is not really needed here since 16 | // we already told to the logger that it should print 17 | // only warning and error messages in production. 18 | const skip = () => { 19 | const env = process.env.NODE_ENV || 'development'; 20 | return env !== 'development'; 21 | }; 22 | 23 | // Build the morgan middleware 24 | const MorganMiddleware = morgan( 25 | // Define message format string (this is the default one). 26 | // The message format is made from tokens, and each token is 27 | // defined inside the Morgan library. 28 | // You can create your custom token to show what do you want from a request. 29 | ':method :url :status :res[content-length] - :response-time ms', 30 | // Options: in this case, I overwrote the stream and the skip logic. 31 | // See the methods above. 32 | { stream, skip } 33 | ); 34 | 35 | export default MorganMiddleware; 36 | -------------------------------------------------------------------------------- /src/api/middlewares/validator/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { validationResult, ValidationChain } from 'express-validator'; 3 | import requirements from './requirements'; 4 | 5 | export const Requirements = requirements; 6 | 7 | export const Validate = (validations: ValidationChain[]) => { 8 | return async (req: Request, res: Response, next: NextFunction) => { 9 | await Promise.all(validations.map((validation) => validation.run(req))); 10 | 11 | const errors = validationResult(req); 12 | if (errors.isEmpty()) { 13 | return next(); 14 | } 15 | 16 | res.status(400).json({ 17 | message: 'Invalid form', 18 | errors: errors.array() 19 | }); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/api/middlewares/validator/requirements/index.ts: -------------------------------------------------------------------------------- 1 | import mainRequirement from './main'; 2 | import usersRequirement from './users'; 3 | import rolesRequirement from './roles'; 4 | 5 | export default { 6 | ...mainRequirement, 7 | ...usersRequirement, 8 | ...rolesRequirement 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/middlewares/validator/requirements/main.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const mainRequirement = { 4 | login: [ 5 | body('email').isEmail(), 6 | body('password').isString().isLength({ min: 5 }) 7 | ], 8 | signup: [ 9 | body('email').isEmail(), 10 | body('password').isString().isLength({ min: 5 }), 11 | body('firstName').isString().isLength({ min: 1 }), 12 | body('lastName').isString().optional({ nullable: true }) 13 | ] 14 | }; 15 | 16 | export default mainRequirement; 17 | -------------------------------------------------------------------------------- /src/api/middlewares/validator/requirements/roles.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const rolesRequirement = { 4 | createRole: [ 5 | body('name').isString(), 6 | body('description').isString().optional({ nullable: true }) 7 | ] 8 | }; 9 | 10 | export default rolesRequirement; 11 | -------------------------------------------------------------------------------- /src/api/middlewares/validator/requirements/users.ts: -------------------------------------------------------------------------------- 1 | import { body, param } from 'express-validator'; 2 | 3 | const usersRequirement = { 4 | createUsers: [ 5 | body('email').isEmail(), 6 | body('password').isString().isLength({ min: 5 }), 7 | body('firstName').isString().isLength({ min: 1 }), 8 | body('lastName').isString().optional({ nullable: true }), 9 | body('roleId').isInt().optional({ nullable: true }) 10 | ], 11 | getUserDetail: [param('id').isInt()], 12 | updateUser: [ 13 | param('id').isInt(), 14 | body('firstName').isString().isLength({ min: 1 }), 15 | body('lastName').isString().optional({ nullable: true }), 16 | body('roleId').isInt().optional({ nullable: true }) 17 | ], 18 | deleteUser: [param('id').isInt()] 19 | }; 20 | 21 | export default usersRequirement; 22 | -------------------------------------------------------------------------------- /src/api/models/Role.ts: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes, Optional } from 'sequelize'; 2 | import { db } from '../../database/config'; 3 | 4 | interface RoleAttributes { 5 | id: number; 6 | name: string; 7 | slug: string; 8 | description?: string; 9 | createdAt?: Date; 10 | updatedAt?: Date; 11 | deletedAt?: Date; 12 | } 13 | 14 | export type RoleInput = Optional; 15 | export type RoleOutput = Required; 16 | 17 | class Role extends Model implements RoleAttributes { 18 | public id!: number; 19 | public name!: string; 20 | public slug!: string; 21 | public description!: string; 22 | 23 | public readonly createdAt!: Date; 24 | public readonly updatedAt!: Date; 25 | public readonly deletedAt!: Date; 26 | } 27 | 28 | Role.init( 29 | { 30 | id: { 31 | type: DataTypes.INTEGER, 32 | primaryKey: true, 33 | autoIncrement: true 34 | }, 35 | name: { 36 | type: DataTypes.STRING, 37 | allowNull: false 38 | }, 39 | slug: { 40 | type: DataTypes.STRING, 41 | allowNull: false 42 | }, 43 | description: { 44 | type: DataTypes.STRING 45 | } 46 | }, 47 | { 48 | tableName: 'roles', 49 | freezeTableName: true, 50 | timestamps: true, 51 | paranoid: true, 52 | sequelize: db 53 | } 54 | ); 55 | 56 | export default Role; 57 | -------------------------------------------------------------------------------- /src/api/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes, Optional } from 'sequelize'; 2 | import { db } from '../../database/config'; 3 | import Role, { RoleOutput } from './Role'; 4 | 5 | interface UserAttributes { 6 | id: number; 7 | roleId?: number; 8 | firstName?: string; 9 | lastName?: string; 10 | email: string; 11 | password: string; 12 | createdAt?: Date; 13 | updatedAt?: Date; 14 | deletedAt?: Date; 15 | role?: RoleOutput | null; 16 | } 17 | 18 | export type UserInput = Optional; 19 | export type UserInputUpdate = Optional< 20 | UserAttributes, 21 | 'id' | 'email' | 'password' 22 | >; 23 | export type UserOutput = Optional; 24 | 25 | class User extends Model implements UserAttributes { 26 | public id!: number; 27 | public roleId!: number; 28 | public firstName!: string; 29 | public lastName!: string; 30 | public email!: string; 31 | public password!: string; 32 | 33 | public readonly createdAt!: Date; 34 | public readonly updatedAt!: Date; 35 | public readonly deletedAt!: Date; 36 | } 37 | 38 | User.init( 39 | { 40 | id: { 41 | type: DataTypes.INTEGER, 42 | primaryKey: true, 43 | autoIncrement: true 44 | }, 45 | roleId: { 46 | type: DataTypes.INTEGER 47 | }, 48 | firstName: { 49 | type: DataTypes.STRING 50 | }, 51 | lastName: { 52 | type: DataTypes.STRING 53 | }, 54 | email: { 55 | type: DataTypes.STRING, 56 | unique: true, 57 | allowNull: false 58 | }, 59 | password: { 60 | type: DataTypes.STRING, 61 | allowNull: false 62 | } 63 | }, 64 | { 65 | tableName: 'users', 66 | freezeTableName: true, 67 | timestamps: true, 68 | paranoid: true, 69 | sequelize: db 70 | } 71 | ); 72 | 73 | User.belongsTo(Role, { 74 | foreignKey: 'roleId', 75 | as: 'role' 76 | }); 77 | 78 | export default User; 79 | -------------------------------------------------------------------------------- /src/api/repositories/RoleRepository.ts: -------------------------------------------------------------------------------- 1 | import Role, { RoleInput, RoleOutput } from '../models/Role'; 2 | 3 | interface IRoleRepository { 4 | createRole(payload: RoleInput): Promise; 5 | getRoles(): Promise; 6 | getRoleBySlug(slug: string): Promise; 7 | } 8 | 9 | class RoleRepository implements IRoleRepository { 10 | createRole(payload: RoleInput): Promise { 11 | return Role.create(payload); 12 | } 13 | 14 | getRoles(): Promise { 15 | return Role.findAll(); 16 | } 17 | 18 | getRoleBySlug(slug: string): Promise { 19 | return Role.findOne({ 20 | where: { 21 | slug: slug 22 | } 23 | }); 24 | } 25 | } 26 | 27 | export default new RoleRepository(); 28 | -------------------------------------------------------------------------------- /src/api/repositories/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import User, { UserInput, UserInputUpdate, UserOutput } from '../models/User'; 2 | import Role from '../models/Role'; 3 | 4 | interface IUserRepository { 5 | createUser(payload: UserInput): Promise; 6 | getUsers(): Promise; 7 | getUserDetail(userId: number): Promise; 8 | getUserByEmail(email: string): Promise; 9 | updateUser(userId: number, payload: UserInputUpdate): Promise; 10 | deleteUser(userId: number): Promise; 11 | } 12 | 13 | class UserRepository implements IUserRepository { 14 | createUser(payload: UserInput): Promise { 15 | return User.create(payload); 16 | } 17 | 18 | getUsers(): Promise { 19 | return User.findAll({ 20 | attributes: ['id', 'roleId', 'firstName', 'lastName', 'email'] 21 | }); 22 | } 23 | 24 | getUserDetail(userId: number): Promise { 25 | return User.findByPk(userId, { 26 | attributes: ['id', 'firstName', 'lastName', 'email'], 27 | include: [ 28 | { 29 | model: Role, 30 | as: 'role', 31 | required: false 32 | } 33 | ] 34 | }); 35 | } 36 | 37 | getUserByEmail(email: string): Promise { 38 | return User.findOne({ 39 | where: { 40 | email: email 41 | } 42 | }); 43 | } 44 | 45 | async updateUser( 46 | userId: number, 47 | payload: UserInputUpdate 48 | ): Promise { 49 | const [updatedUserCount] = await User.update(payload, { 50 | where: { 51 | id: userId 52 | } 53 | }); 54 | return !!updatedUserCount; 55 | } 56 | 57 | async deleteUser(userId: number): Promise { 58 | const deletedUserCount = await User.destroy({ 59 | where: { 60 | id: userId 61 | } 62 | }); 63 | return !!deletedUserCount; 64 | } 65 | } 66 | 67 | export default new UserRepository(); 68 | -------------------------------------------------------------------------------- /src/api/repositories/__test__/RoleRepository.test.ts: -------------------------------------------------------------------------------- 1 | import RoleRepository from '../RoleRepository'; 2 | import Role from '../../models/Role'; 3 | import mockResource from './mockResource'; 4 | 5 | jest.mock('../../models/Role'); 6 | 7 | const MockedRole = jest.mocked(Role, true); 8 | 9 | describe('RoleRepository', () => { 10 | describe('RoleRepository.__createRole', () => { 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('should return role created', async () => { 16 | //arrange 17 | const mockInput = 18 | mockResource.RoleRepository.createRole.POSITIVE_CASE_INPUT; 19 | const mockOutput = 20 | mockResource.RoleRepository.createRole.POSITIVE_CASE_OUTPUT; 21 | 22 | MockedRole.create.mockResolvedValue(mockOutput); 23 | 24 | //act 25 | const result = await RoleRepository.createRole(mockInput); 26 | 27 | //assert 28 | expect(result).toEqual(mockOutput); 29 | expect(MockedRole.create).toHaveBeenCalledTimes(1); 30 | expect(MockedRole.create).toBeCalledWith(mockInput); 31 | }); 32 | }); 33 | 34 | describe('RoleRepository.__getRoles', () => { 35 | beforeEach(() => { 36 | jest.clearAllMocks(); 37 | }); 38 | 39 | it('should return list roles', async () => { 40 | //arrange 41 | const mockOutput: any = 42 | mockResource.RoleRepository.getRoles.POSITIVE_CASE_OUTPUT; 43 | 44 | MockedRole.findAll.mockResolvedValue(mockOutput); 45 | 46 | //act 47 | const result = await RoleRepository.getRoles(); 48 | 49 | //assert 50 | expect(result).toEqual(mockOutput); 51 | expect(MockedRole.findAll).toHaveBeenCalledTimes(1); 52 | expect(MockedRole.findAll).toBeCalledWith(); 53 | }); 54 | }); 55 | 56 | describe('RoleRepository.__getRoleBySlug', () => { 57 | beforeEach(() => { 58 | jest.clearAllMocks(); 59 | }); 60 | 61 | it('should return role detail', async () => { 62 | //arrange 63 | const mockInput = 64 | mockResource.RoleRepository.getRoleBySlug.POSITIVE_CASE_INPUT; 65 | const mockModelOptions = 66 | mockResource.RoleRepository.getRoleBySlug.MODEL_OPTIONS; 67 | const mockOutput: any = 68 | mockResource.RoleRepository.getRoleBySlug.POSITIVE_CASE_OUTPUT; 69 | 70 | MockedRole.findOne.mockResolvedValue(mockOutput); 71 | 72 | //act 73 | const result = await RoleRepository.getRoleBySlug(mockInput.slug); 74 | 75 | //assert 76 | expect(result).toEqual(mockOutput); 77 | expect(MockedRole.findOne).toHaveBeenCalledTimes(1); 78 | expect(MockedRole.findOne).toBeCalledWith(mockModelOptions); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/api/repositories/__test__/UserRepository.test.ts: -------------------------------------------------------------------------------- 1 | import UserRepository from '../UserRepository'; 2 | import User from '../../models/User'; 3 | import Role from '../../models/Role'; 4 | import mockResource from './mockResource'; 5 | 6 | jest.mock('../../models/User'); 7 | 8 | const MockedUser = jest.mocked(User, true); 9 | 10 | describe('UserRepository', () => { 11 | describe('UserRepository.__createUser', () => { 12 | beforeEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | it('should return user created', async () => { 17 | //arrange 18 | const mockInput = 19 | mockResource.UserRepository.createUser.POSITIVE_CASE_INPUT; 20 | const mockOutput = 21 | mockResource.UserRepository.createUser.POSITIVE_CASE_OUTPUT; 22 | 23 | MockedUser.create.mockResolvedValue(mockOutput); 24 | 25 | //act 26 | const result = await UserRepository.createUser(mockInput); 27 | 28 | //assert 29 | expect(result).toEqual(mockOutput); 30 | expect(MockedUser.create).toHaveBeenCalledTimes(1); 31 | expect(MockedUser.create).toBeCalledWith(mockInput); 32 | }); 33 | }); 34 | 35 | describe('UserRepository.__getUsers', () => { 36 | beforeEach(() => { 37 | jest.clearAllMocks(); 38 | }); 39 | 40 | it('should return list users', async () => { 41 | //arrange 42 | const mockModelOptions = 43 | mockResource.UserRepository.getUsers.MODEL_OPTIONS; 44 | const mockOutput: any = 45 | mockResource.UserRepository.getUsers.POSITIVE_CASE_OUTPUT; 46 | 47 | MockedUser.findAll.mockResolvedValue(mockOutput); 48 | 49 | //act 50 | const result = await UserRepository.getUsers(); 51 | 52 | //assert 53 | expect(result).toEqual(mockOutput); 54 | expect(MockedUser.findAll).toHaveBeenCalledTimes(1); 55 | expect(MockedUser.findAll).toBeCalledWith(mockModelOptions); 56 | }); 57 | }); 58 | 59 | describe('UserRepository.__getUserDetail', () => { 60 | beforeEach(() => { 61 | jest.clearAllMocks(); 62 | }); 63 | 64 | it('should return user detail', async () => { 65 | //arrange 66 | const mockInput = 67 | mockResource.UserRepository.getUserDetail.POSITIVE_CASE_INPUT; 68 | const mockModelOptions = 69 | mockResource.UserRepository.getUserDetail.MODEL_OPTIONS; 70 | const mockOutput: any = 71 | mockResource.UserRepository.getUserDetail.POSITIVE_CASE_OUTPUT; 72 | 73 | MockedUser.findByPk.mockResolvedValue(mockOutput); 74 | 75 | //act 76 | const result = await UserRepository.getUserDetail(mockInput.userId); 77 | 78 | //assert 79 | expect(result).toEqual(mockOutput); 80 | expect(MockedUser.findByPk).toHaveBeenCalledTimes(1); 81 | expect(MockedUser.findByPk).toBeCalledWith(mockInput.userId, { 82 | ...mockModelOptions, 83 | include: [ 84 | { 85 | model: Role, 86 | as: 'role', 87 | required: false 88 | } 89 | ] 90 | }); 91 | }); 92 | }); 93 | 94 | describe('UserRepository.__getUserByEmail', () => { 95 | beforeEach(() => { 96 | jest.clearAllMocks(); 97 | }); 98 | 99 | it('should return user detail', async () => { 100 | //arrange 101 | const mockInput = 102 | mockResource.UserRepository.getUserByEmail.POSITIVE_CASE_INPUT; 103 | const mockModelOptions = 104 | mockResource.UserRepository.getUserByEmail.MODEL_OPTIONS; 105 | const mockOutput: any = 106 | mockResource.UserRepository.getUserByEmail.POSITIVE_CASE_OUTPUT; 107 | 108 | MockedUser.findOne.mockResolvedValue(mockOutput); 109 | 110 | //act 111 | const result = await UserRepository.getUserByEmail(mockInput.email); 112 | 113 | //assert 114 | expect(result).toEqual(mockOutput); 115 | expect(MockedUser.findOne).toHaveBeenCalledTimes(1); 116 | expect(MockedUser.findOne).toBeCalledWith(mockModelOptions); 117 | }); 118 | }); 119 | 120 | describe('UserRepository.__updateUser', () => { 121 | beforeEach(() => { 122 | jest.clearAllMocks(); 123 | }); 124 | 125 | it('should return update success', async () => { 126 | //arrange 127 | const mockInput = 128 | mockResource.UserRepository.updateUser.POSITIVE_CASE_INPUT; 129 | const mockModelOptions = 130 | mockResource.UserRepository.updateUser.MODEL_OPTIONS; 131 | const mockModelOutput: any = 132 | mockResource.UserRepository.updateUser.POSITIVE_MODEL_OUTPUT; 133 | const mockOutput = 134 | mockResource.UserRepository.updateUser.POSITIVE_CASE_OUTPUT; 135 | 136 | MockedUser.update.mockResolvedValue(mockModelOutput); 137 | 138 | //act 139 | const result = await UserRepository.updateUser( 140 | mockInput.userId, 141 | mockInput.payload 142 | ); 143 | 144 | //assert 145 | expect(result).toEqual(mockOutput); 146 | expect(MockedUser.update).toHaveBeenCalledTimes(1); 147 | expect(MockedUser.update).toBeCalledWith( 148 | mockInput.payload, 149 | mockModelOptions 150 | ); 151 | }); 152 | }); 153 | 154 | describe('UserRepository.__deleteUser', () => { 155 | beforeEach(() => { 156 | jest.clearAllMocks(); 157 | }); 158 | 159 | it('should return delete success', async () => { 160 | //arrange 161 | const mockInput = 162 | mockResource.UserRepository.deleteUser.POSITIVE_CASE_INPUT; 163 | const mockModelOptions = 164 | mockResource.UserRepository.deleteUser.MODEL_OPTIONS; 165 | const mockModelOutput: any = 166 | mockResource.UserRepository.deleteUser.POSITIVE_MODEL_OUTPUT; 167 | const mockOutput = 168 | mockResource.UserRepository.deleteUser.POSITIVE_CASE_OUTPUT; 169 | 170 | MockedUser.destroy.mockResolvedValue(mockModelOutput); 171 | 172 | //act 173 | const result = await UserRepository.deleteUser(mockInput.userId); 174 | 175 | //assert 176 | expect(result).toEqual(mockOutput); 177 | expect(MockedUser.destroy).toHaveBeenCalledTimes(1); 178 | expect(MockedUser.destroy).toBeCalledWith(mockModelOptions); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /src/api/repositories/__test__/mockResource.ts: -------------------------------------------------------------------------------- 1 | const mockResource = { 2 | RoleRepository: { 3 | createRole: { 4 | POSITIVE_CASE_INPUT: { 5 | name: 'Admin', 6 | description: 'Admin', 7 | slug: 'admin' 8 | }, 9 | POSITIVE_CASE_OUTPUT: { 10 | id: 1, 11 | name: 'Admin', 12 | description: 'Admin', 13 | slug: 'admin', 14 | createdAt: '2022-08-26 14:40:19', 15 | updatedAt: '2022-08-26 14:40:19', 16 | deletedAt: null 17 | } 18 | }, 19 | getRoles: { 20 | POSITIVE_CASE_OUTPUT: [ 21 | { 22 | id: 1, 23 | name: 'Admin', 24 | description: 'Admin', 25 | slug: 'admin', 26 | createdAt: '2022-08-26 14:40:19', 27 | updatedAt: '2022-08-26 14:40:19', 28 | deletedAt: null 29 | } 30 | ] 31 | }, 32 | getRoleBySlug: { 33 | POSITIVE_CASE_INPUT: { 34 | slug: 'admin' 35 | }, 36 | MODEL_OPTIONS: { 37 | where: { 38 | slug: 'admin' 39 | } 40 | }, 41 | POSITIVE_CASE_OUTPUT: { 42 | id: 1, 43 | name: 'Admin', 44 | description: 'Admin', 45 | slug: 'admin', 46 | createdAt: '2022-08-26 14:40:19', 47 | updatedAt: '2022-08-26 14:40:19', 48 | deletedAt: null 49 | } 50 | } 51 | }, 52 | UserRepository: { 53 | createUser: { 54 | POSITIVE_CASE_INPUT: { 55 | email: 'user@mail.com', 56 | password: 57 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 58 | firstName: 'John', 59 | lastName: 'Doe', 60 | roleId: 1 61 | }, 62 | POSITIVE_CASE_OUTPUT: { 63 | id: 1, 64 | email: 'user@mail.com', 65 | password: 66 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 67 | firstName: 'John', 68 | lastName: 'Doe', 69 | roleId: 1, 70 | createdAt: '2022-08-26 14:40:19', 71 | updatedAt: '2022-08-26 14:40:19', 72 | deletedAt: null 73 | } 74 | }, 75 | getUsers: { 76 | MODEL_OPTIONS: { 77 | attributes: ['id', 'roleId', 'firstName', 'lastName', 'email'] 78 | }, 79 | POSITIVE_CASE_OUTPUT: [ 80 | { 81 | id: 1, 82 | email: 'user@mail.com', 83 | firstName: 'John', 84 | lastName: 'Doe', 85 | roleId: 1 86 | } 87 | ] 88 | }, 89 | getUserDetail: { 90 | POSITIVE_CASE_INPUT: { 91 | userId: 1 92 | }, 93 | MODEL_OPTIONS: { 94 | attributes: ['id', 'firstName', 'lastName', 'email'] 95 | }, 96 | POSITIVE_CASE_OUTPUT: { 97 | id: 1, 98 | email: 'user@mail.com', 99 | firstName: 'John', 100 | lastName: 'Doe', 101 | role: { 102 | id: 1, 103 | name: 'Admin', 104 | description: 'Admin', 105 | slug: 'admin', 106 | createdAt: '2022-08-26 14:40:19', 107 | updatedAt: '2022-08-26 14:40:19', 108 | deletedAt: null 109 | } 110 | } 111 | }, 112 | getUserByEmail: { 113 | POSITIVE_CASE_INPUT: { 114 | email: 'user@mail.com' 115 | }, 116 | MODEL_OPTIONS: { 117 | where: { 118 | email: 'user@mail.com' 119 | } 120 | }, 121 | POSITIVE_CASE_OUTPUT: { 122 | id: 1, 123 | email: 'user@mail.com', 124 | password: 125 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 126 | firstName: 'John', 127 | lastName: 'Doe', 128 | roleId: 1, 129 | createdAt: '2022-08-26 14:40:19', 130 | updatedAt: '2022-08-26 14:40:19', 131 | deletedAt: null 132 | } 133 | }, 134 | updateUser: { 135 | POSITIVE_CASE_INPUT: { 136 | userId: 1, 137 | payload: { 138 | email: 'user@mail.com', 139 | firstName: 'John', 140 | lastName: 'Doe', 141 | roleId: 1 142 | } 143 | }, 144 | MODEL_OPTIONS: { 145 | where: { 146 | id: 1 147 | } 148 | }, 149 | POSITIVE_MODEL_OUTPUT: [1], 150 | POSITIVE_CASE_OUTPUT: true 151 | }, 152 | deleteUser: { 153 | POSITIVE_CASE_INPUT: { 154 | userId: 1 155 | }, 156 | MODEL_OPTIONS: { 157 | where: { 158 | id: 1 159 | } 160 | }, 161 | POSITIVE_MODEL_OUTPUT: 1, 162 | POSITIVE_CASE_OUTPUT: true 163 | } 164 | } 165 | }; 166 | 167 | export default mockResource; 168 | -------------------------------------------------------------------------------- /src/api/routes/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import mainRouter from './main'; 3 | import usersRouter from './users'; 4 | import rolesRouter from './roles'; 5 | 6 | const router: Router = Router(); 7 | router.use('/', mainRouter); 8 | router.use('/users', usersRouter); 9 | router.use('/roles', rolesRouter); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/api/routes/v1/main.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import AuthController from '../../controllers/AuthController'; 3 | import { Validate, Requirements } from '../../middlewares/validator'; 4 | 5 | const mainRouter: Router = Router(); 6 | 7 | mainRouter 8 | .route('/login') 9 | .post(Validate(Requirements.login), AuthController.login); 10 | mainRouter 11 | .route('/signup') 12 | .post(Validate(Requirements.signup), AuthController.signUp); 13 | 14 | export default mainRouter; 15 | -------------------------------------------------------------------------------- /src/api/routes/v1/roles.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import RoleController from '../../controllers/RoleController'; 3 | import Auth from '../../middlewares/auth'; 4 | import { Validate, Requirements } from '../../middlewares/validator'; 5 | import { ROLE } from '../../../constants'; 6 | 7 | const rolesRouter: Router = Router(); 8 | 9 | rolesRouter 10 | .route('/') 11 | .post( 12 | Auth.authenticate, 13 | Validate(Requirements.createRole), 14 | Auth.checkRoles(ROLE.ADMIN), 15 | RoleController.createRole 16 | ) 17 | .get( 18 | Auth.authenticate, 19 | Auth.checkRoles(ROLE.ADMIN), 20 | RoleController.getRoles 21 | ); 22 | 23 | export default rolesRouter; 24 | -------------------------------------------------------------------------------- /src/api/routes/v1/users.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import UserController from '../../controllers/UserController'; 3 | import Auth from '../../middlewares/auth'; 4 | import { Validate, Requirements } from '../../middlewares/validator'; 5 | import { ROLE } from '../../../constants'; 6 | 7 | const usersRouter: Router = Router(); 8 | 9 | usersRouter 10 | .route('/') 11 | .post( 12 | Auth.authenticate, 13 | Validate(Requirements.createUsers), 14 | Auth.checkRoles(ROLE.ADMIN), 15 | UserController.createUser 16 | ) 17 | .get( 18 | Auth.authenticate, 19 | Auth.checkRoles(ROLE.ADMIN), 20 | UserController.getUsers 21 | ); 22 | 23 | usersRouter 24 | .route('/:id') 25 | .get( 26 | Auth.authenticate, 27 | Validate(Requirements.getUserDetail), 28 | Auth.checkRoles(ROLE.ADMIN), 29 | UserController.getUserDetail 30 | ) 31 | .put( 32 | Auth.authenticate, 33 | Validate(Requirements.updateUser), 34 | Auth.checkRoles(ROLE.ADMIN), 35 | UserController.updateUser 36 | ) 37 | .delete( 38 | Auth.authenticate, 39 | Validate(Requirements.deleteUser), 40 | Auth.checkRoles(ROLE.ADMIN), 41 | UserController.deleteUser 42 | ); 43 | 44 | export default usersRouter; 45 | -------------------------------------------------------------------------------- /src/api/services/AuthService.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import UserRepository from '../repositories/UserRepository'; 3 | import { UserInput, UserOutput } from '../models/User'; 4 | import JWT from '../../utils/jwt'; 5 | 6 | interface IAuthService { 7 | login(payload: UserInput): Promise; 8 | signUp(payload: UserInput): Promise; 9 | } 10 | 11 | class AuthService implements IAuthService { 12 | async login(payload: UserInput): Promise { 13 | const user = await UserRepository.getUserByEmail(payload.email); 14 | 15 | if (!user) { 16 | throw new Error('User not found'); 17 | } 18 | 19 | const isValid = bcrypt.compareSync(payload.password, user.password); 20 | 21 | if (!isValid) { 22 | throw new Error('Email and Password is not match'); 23 | } 24 | 25 | const token = await JWT.signToken(user.id); 26 | 27 | if (!token) { 28 | throw new Error('Invalid token'); 29 | } 30 | 31 | return token; 32 | } 33 | 34 | async signUp(payload: UserInput): Promise { 35 | const user = await UserRepository.getUserByEmail(payload.email); 36 | 37 | if (user) { 38 | throw new Error('Email must be unique'); 39 | } 40 | 41 | const hashedPassword = bcrypt.hashSync(payload.password, 5); 42 | 43 | return UserRepository.createUser({ 44 | ...payload, 45 | password: hashedPassword 46 | }); 47 | } 48 | } 49 | 50 | export default new AuthService(); 51 | -------------------------------------------------------------------------------- /src/api/services/RoleService.ts: -------------------------------------------------------------------------------- 1 | import RoleRepository from '../repositories/RoleRepository'; 2 | import { RoleInput, RoleOutput } from '../models/Role'; 3 | import { slugify } from '../../utils/helpers'; 4 | 5 | interface IRoleService { 6 | createRole(payload: RoleInput): Promise; 7 | getRoles(): Promise; 8 | } 9 | 10 | class RoleService implements IRoleService { 11 | async createRole(payload: RoleInput): Promise { 12 | const slug = slugify(payload.name); 13 | const role = await RoleRepository.getRoleBySlug(slug); 14 | 15 | if (role) { 16 | throw new Error('Role is exist'); 17 | } 18 | 19 | return RoleRepository.createRole({ 20 | ...payload, 21 | slug 22 | }); 23 | } 24 | 25 | getRoles(): Promise { 26 | return RoleRepository.getRoles(); 27 | } 28 | } 29 | 30 | export default new RoleService(); 31 | -------------------------------------------------------------------------------- /src/api/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import UserRepository from '../repositories/UserRepository'; 3 | import { UserInput, UserInputUpdate, UserOutput } from '../models/User'; 4 | 5 | interface IUserService { 6 | createUser(payload: UserInput): Promise; 7 | getUsers(): Promise; 8 | getUserDetail(userId: number): Promise; 9 | updateUser(userId: number, data: UserInputUpdate): Promise; 10 | deleteUser(userId: number): Promise; 11 | } 12 | 13 | class UserService implements IUserService { 14 | async createUser(payload: UserInput): Promise { 15 | const user = await UserRepository.getUserByEmail(payload.email); 16 | 17 | if (user) { 18 | throw new Error('Email must be unique'); 19 | } 20 | 21 | const hashedPassword = bcrypt.hashSync(payload.password, 5); 22 | 23 | return UserRepository.createUser({ 24 | ...payload, 25 | password: hashedPassword 26 | }); 27 | } 28 | 29 | getUsers(): Promise { 30 | return UserRepository.getUsers(); 31 | } 32 | 33 | async getUserDetail(userId: number): Promise { 34 | const user = await UserRepository.getUserDetail(userId); 35 | 36 | if (!user) { 37 | throw new Error('User not found'); 38 | } 39 | 40 | return user; 41 | } 42 | 43 | async updateUser( 44 | userId: number, 45 | payload: UserInputUpdate 46 | ): Promise { 47 | const user = await UserRepository.getUserDetail(userId); 48 | 49 | if (!user) { 50 | throw new Error('User not found'); 51 | } 52 | 53 | return UserRepository.updateUser(userId, payload); 54 | } 55 | 56 | async deleteUser(userId: number): Promise { 57 | const user = await UserRepository.getUserDetail(userId); 58 | 59 | if (!user) { 60 | throw new Error('User not found'); 61 | } 62 | 63 | return UserRepository.deleteUser(userId); 64 | } 65 | } 66 | 67 | export default new UserService(); 68 | -------------------------------------------------------------------------------- /src/api/services/__test__/AuthService.test.ts: -------------------------------------------------------------------------------- 1 | import AuthService from '../AuthService'; 2 | import UserRepository from '../../repositories/UserRepository'; 3 | import mockResource from './mockResource'; 4 | import * as bcrypt from 'bcrypt'; 5 | import JWT from '../../../utils/jwt'; 6 | 7 | jest.mock('../../repositories/UserRepository'); 8 | jest.mock('../../../utils/jwt'); 9 | jest.mock('bcrypt'); 10 | 11 | const MockedUserRepository = jest.mocked(UserRepository, true); 12 | const MockedBycrypt = jest.mocked(bcrypt, true); 13 | const MockedJWT = jest.mocked(JWT, true); 14 | 15 | describe('AuthService', () => { 16 | describe('AuthService.__login', () => { 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('should return success login', async () => { 22 | //arrange 23 | const mockInput = 24 | mockResource.AuthService.login.POSITIVE_CASE_INPUT; 25 | const mockUserEmailOutput: any = 26 | mockResource.AuthService.login.CASE_EXIST_USER_EMAIL; 27 | const mockCompareOutput = 28 | mockResource.AuthService.login.CASE_VALID_COMPARE; 29 | const mockTokenOutput = 30 | mockResource.AuthService.login.CASE_VALID_TOKEN; 31 | 32 | MockedUserRepository.getUserByEmail.mockResolvedValue( 33 | mockUserEmailOutput 34 | ); 35 | MockedBycrypt.compareSync.mockReturnValue(mockCompareOutput); 36 | MockedJWT.signToken.mockResolvedValue(mockTokenOutput); 37 | 38 | //act 39 | const result = await AuthService.login(mockInput); 40 | 41 | //assert 42 | expect(result).toEqual(mockTokenOutput); 43 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 44 | 1 45 | ); 46 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 47 | mockInput.email 48 | ); 49 | 50 | expect(MockedBycrypt.compareSync).toHaveBeenCalledTimes(1); 51 | expect(MockedBycrypt.compareSync).toBeCalledWith( 52 | mockInput.password, 53 | mockUserEmailOutput.password 54 | ); 55 | 56 | expect(MockedJWT.signToken).toHaveBeenCalledTimes(1); 57 | expect(MockedJWT.signToken).toBeCalledWith(mockUserEmailOutput.id); 58 | }); 59 | 60 | it('should return error user not found', () => { 61 | //arrange 62 | const mockInput = 63 | mockResource.AuthService.login.POSITIVE_CASE_INPUT; 64 | const mockUserEmailOutput: any = 65 | mockResource.AuthService.login.CASE_NULL_USER_EMAIL; 66 | const errorMessage = 67 | mockResource.AuthService.login.ERR_USER_NOT_FOUND; 68 | 69 | MockedUserRepository.getUserByEmail.mockResolvedValue( 70 | mockUserEmailOutput 71 | ); 72 | 73 | //act 74 | const result = AuthService.login(mockInput); 75 | 76 | //assert 77 | expect(result).rejects.toThrowError(errorMessage); 78 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 79 | 1 80 | ); 81 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 82 | mockInput.email 83 | ); 84 | }); 85 | 86 | it('should return error invalid email and password', () => { 87 | //arrange 88 | const mockInput = 89 | mockResource.AuthService.login.POSITIVE_CASE_INPUT; 90 | const mockUserEmailOutput: any = 91 | mockResource.AuthService.login.CASE_EXIST_USER_EMAIL; 92 | const mockCompareOutput = 93 | mockResource.AuthService.login.CASE_INVALID_COMPARE; 94 | const errorMessage = 95 | mockResource.AuthService.login.ERR_INVALID_PASSWORD; 96 | 97 | MockedUserRepository.getUserByEmail.mockResolvedValue( 98 | mockUserEmailOutput 99 | ); 100 | MockedBycrypt.compareSync.mockReturnValue(mockCompareOutput); 101 | 102 | //act 103 | const result = AuthService.login(mockInput); 104 | 105 | //assert 106 | expect(result).rejects.toThrowError(errorMessage); 107 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 108 | 1 109 | ); 110 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 111 | mockInput.email 112 | ); 113 | }); 114 | 115 | it('should return error invalid token', () => { 116 | //arrange 117 | const mockInput = 118 | mockResource.AuthService.login.POSITIVE_CASE_INPUT; 119 | const mockUserEmailOutput: any = 120 | mockResource.AuthService.login.CASE_EXIST_USER_EMAIL; 121 | const mockCompareOutput = 122 | mockResource.AuthService.login.CASE_VALID_COMPARE; 123 | const mockTokenOutput = 124 | mockResource.AuthService.login.CASE_UNDEFINED_TOKEN; 125 | const errorMessage = 126 | mockResource.AuthService.login.ERR_INVALID_TOKEN; 127 | 128 | MockedUserRepository.getUserByEmail.mockResolvedValue( 129 | mockUserEmailOutput 130 | ); 131 | MockedBycrypt.compareSync.mockReturnValue(mockCompareOutput); 132 | MockedJWT.signToken.mockResolvedValue(mockTokenOutput); 133 | 134 | //act 135 | const result = AuthService.login(mockInput); 136 | 137 | //assert 138 | expect(result).rejects.toThrowError(errorMessage); 139 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 140 | 1 141 | ); 142 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 143 | mockInput.email 144 | ); 145 | }); 146 | }); 147 | 148 | describe('AuthService.__signUp', () => { 149 | beforeEach(() => { 150 | jest.clearAllMocks(); 151 | }); 152 | 153 | it('should return success login', async () => { 154 | //arrange 155 | const mockInput = 156 | mockResource.AuthService.signUp.POSITIVE_CASE_INPUT; 157 | const mockUserEmailOutput = 158 | mockResource.AuthService.signUp.CASE_NULL_USER_EMAIL; 159 | const mockHashOutput = 160 | mockResource.AuthService.signUp.BCRYPT_HASH_OUTPUT; 161 | const mockOutput: any = 162 | mockResource.AuthService.signUp.POSITIVE_CASE_OUTPUT; 163 | 164 | MockedUserRepository.getUserByEmail.mockResolvedValue( 165 | mockUserEmailOutput 166 | ); 167 | MockedBycrypt.hashSync.mockReturnValue(mockHashOutput); 168 | MockedUserRepository.createUser.mockResolvedValue(mockOutput); 169 | 170 | //act 171 | const result = await AuthService.signUp(mockInput); 172 | 173 | //assert 174 | expect(result).toEqual(mockOutput); 175 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 176 | 1 177 | ); 178 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 179 | mockInput.email 180 | ); 181 | 182 | expect(MockedBycrypt.hashSync).toHaveBeenCalledTimes(1); 183 | expect(MockedBycrypt.hashSync).toBeCalledWith( 184 | mockInput.password, 185 | 5 186 | ); 187 | 188 | expect(MockedUserRepository.createUser).toHaveBeenCalledTimes(1); 189 | expect(MockedUserRepository.createUser).toBeCalledWith({ 190 | ...mockInput, 191 | password: mockHashOutput 192 | }); 193 | }); 194 | 195 | it('should return error email exist', () => { 196 | //arrange 197 | const mockInput = 198 | mockResource.AuthService.signUp.POSITIVE_CASE_INPUT; 199 | const mockUserEmailOutput: any = 200 | mockResource.AuthService.signUp.CASE_EXIST_USER_EMAIL; 201 | const errorMessage = mockResource.AuthService.signUp.ERROR_MESSAGE; 202 | 203 | MockedUserRepository.getUserByEmail.mockResolvedValue( 204 | mockUserEmailOutput 205 | ); 206 | 207 | //act 208 | const result = AuthService.signUp(mockInput); 209 | 210 | //assert 211 | expect(result).rejects.toThrowError(errorMessage); 212 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 213 | 1 214 | ); 215 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 216 | mockInput.email 217 | ); 218 | }); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/api/services/__test__/RoleService.test.ts: -------------------------------------------------------------------------------- 1 | import RoleService from '../RoleService'; 2 | import RoleRepository from '../../repositories/RoleRepository'; 3 | import mockResource from './mockResource'; 4 | import { slugify } from '../../../utils/helpers'; 5 | 6 | jest.mock('../../repositories/RoleRepository'); 7 | jest.mock('../../../utils/helpers'); 8 | 9 | const MockedRoleRepository = jest.mocked(RoleRepository, true); 10 | const MockedSlugify = jest.mocked(slugify, true); 11 | 12 | describe('RoleService', () => { 13 | describe('RoleService.__createRole', () => { 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | }); 17 | 18 | it('should return role created', async () => { 19 | //arrange 20 | const mockInput = 21 | mockResource.RoleService.createRole.POSITIVE_CASE_INPUT; 22 | const mockOutputSlugify = 23 | mockResource.RoleService.createRole.SLUGIFY_OUTPUT; 24 | const mockOutputRoleSlug = 25 | mockResource.RoleService.createRole.CASE_NULL_ROLE_SLUG; 26 | const mockOutput: any = 27 | mockResource.RoleService.createRole.POSITIVE_CASE_OUTPUT; 28 | 29 | MockedSlugify.mockReturnValue(mockOutputSlugify); 30 | MockedRoleRepository.getRoleBySlug.mockResolvedValue( 31 | mockOutputRoleSlug 32 | ); 33 | MockedRoleRepository.createRole.mockResolvedValue(mockOutput); 34 | 35 | //act 36 | const result = await RoleService.createRole(mockInput); 37 | 38 | //assert 39 | expect(result).toEqual(mockOutput); 40 | expect(MockedSlugify).toHaveBeenCalledTimes(1); 41 | expect(MockedSlugify).toBeCalledWith(mockInput.name); 42 | 43 | expect(MockedRoleRepository.getRoleBySlug).toHaveBeenCalledTimes(1); 44 | expect(MockedRoleRepository.getRoleBySlug).toBeCalledWith( 45 | mockOutputSlugify 46 | ); 47 | 48 | expect(MockedRoleRepository.createRole).toHaveBeenCalledTimes(1); 49 | expect(MockedRoleRepository.createRole).toBeCalledWith({ 50 | ...mockInput, 51 | slug: mockOutputSlugify 52 | }); 53 | }); 54 | 55 | it('should return error role exist', () => { 56 | //arrange 57 | const mockInput = 58 | mockResource.RoleService.createRole.POSITIVE_CASE_INPUT; 59 | const mockOutputSlugify = 60 | mockResource.RoleService.createRole.SLUGIFY_OUTPUT; 61 | const mockOutputRoleSlug: any = 62 | mockResource.RoleService.createRole.CASE_EXIST_ROLE_SLUG; 63 | const errorMessage = 64 | mockResource.RoleService.createRole.ERROR_MESSAGE; 65 | 66 | MockedSlugify.mockReturnValue(mockOutputSlugify); 67 | MockedRoleRepository.getRoleBySlug.mockResolvedValue( 68 | mockOutputRoleSlug 69 | ); 70 | 71 | //act 72 | const result = RoleService.createRole(mockInput); 73 | 74 | //assert 75 | expect(result).rejects.toThrowError(errorMessage); 76 | expect(MockedSlugify).toHaveBeenCalledTimes(1); 77 | expect(MockedSlugify).toBeCalledWith(mockInput.name); 78 | 79 | expect(MockedRoleRepository.getRoleBySlug).toHaveBeenCalledTimes(1); 80 | expect(MockedRoleRepository.getRoleBySlug).toBeCalledWith( 81 | mockOutputSlugify 82 | ); 83 | }); 84 | }); 85 | 86 | describe('RoleService.__getRoles', () => { 87 | beforeEach(() => { 88 | jest.clearAllMocks(); 89 | }); 90 | 91 | it('should return list roles', async () => { 92 | //arrange 93 | const mockOutput: any = 94 | mockResource.RoleService.getRoles.POSITIVE_CASE_OUTPUT; 95 | 96 | MockedRoleRepository.getRoles.mockResolvedValue(mockOutput); 97 | 98 | //act 99 | const result = await RoleService.getRoles(); 100 | 101 | //assert 102 | expect(result).toEqual(mockOutput); 103 | expect(MockedRoleRepository.getRoles).toHaveBeenCalledTimes(1); 104 | expect(MockedRoleRepository.getRoles).toBeCalledWith(); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/api/services/__test__/UserService.test.ts: -------------------------------------------------------------------------------- 1 | import UserService from '../UserService'; 2 | import UserRepository from '../../repositories/UserRepository'; 3 | import mockResource from './mockResource'; 4 | import * as bcrypt from 'bcrypt'; 5 | 6 | jest.mock('../../repositories/UserRepository'); 7 | jest.mock('bcrypt'); 8 | 9 | const MockedUserRepository = jest.mocked(UserRepository, true); 10 | const MockedBycrypt = jest.mocked(bcrypt, true); 11 | 12 | describe('UserService', () => { 13 | describe('UserService.__createUser', () => { 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | }); 17 | 18 | it('should return user created', async () => { 19 | //arrange 20 | const mockInput = 21 | mockResource.UserService.createUser.POSITIVE_CASE_INPUT; 22 | const mockOutputBcryptHash = 23 | mockResource.UserService.createUser.BCRYPT_HASH_OUTPUT; 24 | const mockOutputUserEmail = 25 | mockResource.UserService.createUser.CASE_NULL_USER_EMAIL; 26 | const mockOutput: any = 27 | mockResource.UserService.createUser.POSITIVE_CASE_OUTPUT; 28 | 29 | MockedUserRepository.getUserByEmail.mockResolvedValue( 30 | mockOutputUserEmail 31 | ); 32 | MockedBycrypt.hashSync.mockReturnValue(mockOutputBcryptHash); 33 | MockedUserRepository.createUser.mockResolvedValue(mockOutput); 34 | 35 | //act 36 | const result = await UserService.createUser(mockInput); 37 | 38 | //assert 39 | expect(result).toEqual(mockOutput); 40 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 41 | 1 42 | ); 43 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 44 | mockInput.email 45 | ); 46 | 47 | expect(MockedBycrypt.hashSync).toHaveBeenCalledTimes(1); 48 | expect(MockedBycrypt.hashSync).toBeCalledWith( 49 | mockInput.password, 50 | 5 51 | ); 52 | 53 | expect(MockedUserRepository.createUser).toHaveBeenCalledTimes(1); 54 | expect(MockedUserRepository.createUser).toBeCalledWith({ 55 | ...mockInput, 56 | password: mockOutputBcryptHash 57 | }); 58 | }); 59 | 60 | it('should return error user exist', () => { 61 | //arrange 62 | const mockInput = 63 | mockResource.UserService.createUser.POSITIVE_CASE_INPUT; 64 | const mockOutputUserEmail: any = 65 | mockResource.UserService.createUser.CASE_EXIST_USER_EMAIL; 66 | const errorMessage = 67 | mockResource.UserService.createUser.ERROR_MESSAGE; 68 | 69 | MockedUserRepository.getUserByEmail.mockResolvedValue( 70 | mockOutputUserEmail 71 | ); 72 | 73 | //act 74 | const result = UserService.createUser(mockInput); 75 | 76 | //assert 77 | expect(result).rejects.toThrowError(errorMessage); 78 | 79 | expect(MockedUserRepository.getUserByEmail).toHaveBeenCalledTimes( 80 | 1 81 | ); 82 | expect(MockedUserRepository.getUserByEmail).toBeCalledWith( 83 | mockInput.email 84 | ); 85 | }); 86 | }); 87 | 88 | describe('UserService.__getUsers', () => { 89 | beforeEach(() => { 90 | jest.clearAllMocks(); 91 | }); 92 | 93 | it('should return list users', async () => { 94 | //arrange 95 | const mockOutput: any = 96 | mockResource.UserService.getUsers.POSITIVE_CASE_OUTPUT; 97 | 98 | MockedUserRepository.getUsers.mockResolvedValue(mockOutput); 99 | 100 | //act 101 | const result = await UserService.getUsers(); 102 | 103 | //assert 104 | expect(result).toEqual(mockOutput); 105 | expect(MockedUserRepository.getUsers).toHaveBeenCalledTimes(1); 106 | expect(MockedUserRepository.getUsers).toBeCalledWith(); 107 | }); 108 | }); 109 | 110 | describe('UserService.__getUserDetail', () => { 111 | beforeEach(() => { 112 | jest.clearAllMocks(); 113 | }); 114 | 115 | it('should return user detail', async () => { 116 | //arrange 117 | const mockInput = 118 | mockResource.UserService.getUserDetail.POSITIVE_CASE_INPUT; 119 | const mockOutput: any = 120 | mockResource.UserService.getUserDetail.POSITIVE_CASE_OUTPUT; 121 | 122 | MockedUserRepository.getUserDetail.mockResolvedValue(mockOutput); 123 | 124 | //act 125 | const result = await UserService.getUserDetail(mockInput.userId); 126 | 127 | //assert 128 | expect(result).toEqual(mockOutput); 129 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 130 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 131 | mockInput.userId 132 | ); 133 | }); 134 | 135 | it('should return error user not found', () => { 136 | //arrange 137 | const mockInput = 138 | mockResource.UserService.getUserDetail.POSITIVE_CASE_INPUT; 139 | const mockOutput = 140 | mockResource.UserService.getUserDetail.CASE_NULL_OUPUT; 141 | const errorMessage = 142 | mockResource.UserService.getUserDetail.ERROR_MESSAGE; 143 | 144 | MockedUserRepository.getUserDetail.mockResolvedValue(mockOutput); 145 | 146 | //act 147 | const result = UserService.getUserDetail(mockInput.userId); 148 | 149 | //assert 150 | expect(result).rejects.toThrowError(errorMessage); 151 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 152 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 153 | mockInput.userId 154 | ); 155 | }); 156 | }); 157 | 158 | describe('UserService.__updateUser', () => { 159 | beforeEach(() => { 160 | jest.clearAllMocks(); 161 | }); 162 | 163 | it('should return success update user', async () => { 164 | //arrange 165 | const mockInput = 166 | mockResource.UserService.updateUser.POSITIVE_CASE_INPUT; 167 | const mockOutputDetail: any = 168 | mockResource.UserService.updateUser.CASE_EXIST_DETAIL; 169 | const mockOutput = 170 | mockResource.UserService.updateUser.POSITIVE_CASE_OUTPUT; 171 | 172 | MockedUserRepository.getUserDetail.mockResolvedValue( 173 | mockOutputDetail 174 | ); 175 | MockedUserRepository.updateUser.mockResolvedValue(mockOutput); 176 | 177 | //act 178 | const result = await UserService.updateUser( 179 | mockInput.userId, 180 | mockInput.payload 181 | ); 182 | 183 | //assert 184 | expect(result).toEqual(mockOutput); 185 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 186 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 187 | mockInput.userId 188 | ); 189 | 190 | expect(MockedUserRepository.updateUser).toHaveBeenCalledTimes(1); 191 | expect(MockedUserRepository.updateUser).toBeCalledWith( 192 | mockInput.userId, 193 | mockInput.payload 194 | ); 195 | }); 196 | 197 | it('should return error user not found', () => { 198 | //arrange 199 | const mockInput = 200 | mockResource.UserService.updateUser.POSITIVE_CASE_INPUT; 201 | const mockOutputDetail = 202 | mockResource.UserService.updateUser.CASE_NULL_DETAIL; 203 | const errorMessage = 204 | mockResource.UserService.updateUser.ERROR_MESSAGE; 205 | 206 | MockedUserRepository.getUserDetail.mockResolvedValue( 207 | mockOutputDetail 208 | ); 209 | 210 | //act 211 | const result = UserService.updateUser( 212 | mockInput.userId, 213 | mockInput.payload 214 | ); 215 | 216 | //assert 217 | expect(result).rejects.toThrowError(errorMessage); 218 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 219 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 220 | mockInput.userId 221 | ); 222 | }); 223 | }); 224 | 225 | describe('UserService.__deleteUser', () => { 226 | beforeEach(() => { 227 | jest.clearAllMocks(); 228 | }); 229 | 230 | it('should return success deelte user', async () => { 231 | //arrange 232 | const mockInput = 233 | mockResource.UserService.deleteUser.POSITIVE_CASE_INPUT; 234 | const mockOutputDetail: any = 235 | mockResource.UserService.deleteUser.CASE_EXIST_DETAIL; 236 | const mockOutput = 237 | mockResource.UserService.deleteUser.POSITIVE_CASE_OUTPUT; 238 | 239 | MockedUserRepository.getUserDetail.mockResolvedValue( 240 | mockOutputDetail 241 | ); 242 | MockedUserRepository.deleteUser.mockResolvedValue(mockOutput); 243 | 244 | //act 245 | const result = await UserService.deleteUser(mockInput.userId); 246 | 247 | //assert 248 | expect(result).toEqual(mockOutput); 249 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 250 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 251 | mockInput.userId 252 | ); 253 | 254 | expect(MockedUserRepository.deleteUser).toHaveBeenCalledTimes(1); 255 | expect(MockedUserRepository.deleteUser).toBeCalledWith( 256 | mockInput.userId 257 | ); 258 | }); 259 | 260 | it('should return error user not found', () => { 261 | //arrange 262 | const mockInput = 263 | mockResource.UserService.deleteUser.POSITIVE_CASE_INPUT; 264 | const mockOutputDetail = 265 | mockResource.UserService.deleteUser.CASE_NULL_DETAIL; 266 | const errorMessage = 267 | mockResource.UserService.deleteUser.ERROR_MESSAGE; 268 | 269 | MockedUserRepository.getUserDetail.mockResolvedValue( 270 | mockOutputDetail 271 | ); 272 | 273 | //act 274 | const result = UserService.deleteUser(mockInput.userId); 275 | 276 | //assert 277 | expect(result).rejects.toThrowError(errorMessage); 278 | expect(MockedUserRepository.getUserDetail).toHaveBeenCalledTimes(1); 279 | expect(MockedUserRepository.getUserDetail).toBeCalledWith( 280 | mockInput.userId 281 | ); 282 | }); 283 | }); 284 | }); 285 | -------------------------------------------------------------------------------- /src/api/services/__test__/mockResource.ts: -------------------------------------------------------------------------------- 1 | const mockResource = { 2 | AuthService: { 3 | login: { 4 | POSITIVE_CASE_INPUT: { 5 | email: 'user@mail.com', 6 | password: 'password' 7 | }, 8 | CASE_NULL_USER_EMAIL: null, 9 | CASE_EXIST_USER_EMAIL: { 10 | id: 1, 11 | email: 'user@mail.com', 12 | password: 13 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 14 | firstName: 'John', 15 | lastName: 'Doe', 16 | roleId: 1, 17 | createdAt: '2022-08-26 14:40:19', 18 | updatedAt: '2022-08-26 14:40:19', 19 | deletedAt: null 20 | }, 21 | CASE_VALID_COMPARE: true, 22 | CASE_INVALID_COMPARE: false, 23 | CASE_VALID_TOKEN: 'abc', 24 | CASE_UNDEFINED_TOKEN: undefined, 25 | ERR_USER_NOT_FOUND: 'User not found', 26 | ERR_INVALID_PASSWORD: 'Email and Password is not match', 27 | ERR_INVALID_TOKEN: 'Invalid token' 28 | }, 29 | signUp: { 30 | POSITIVE_CASE_INPUT: { 31 | email: 'user@mail.com', 32 | password: 'password', 33 | firstName: 'John', 34 | lastName: 'Doe' 35 | }, 36 | CASE_NULL_USER_EMAIL: null, 37 | CASE_EXIST_USER_EMAIL: { 38 | id: 1, 39 | email: 'user@mail.com', 40 | password: 41 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 42 | firstName: 'John', 43 | lastName: 'Doe', 44 | roleId: 1, 45 | createdAt: '2022-08-26 14:40:19', 46 | updatedAt: '2022-08-26 14:40:19', 47 | deletedAt: null 48 | }, 49 | BCRYPT_HASH_OUTPUT: 50 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 51 | POSITIVE_CASE_OUTPUT: { 52 | id: 1, 53 | email: 'user@mail.com', 54 | password: 55 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 56 | firstName: 'John', 57 | lastName: 'Doe', 58 | roleId: 1, 59 | createdAt: '2022-08-26 14:40:19', 60 | updatedAt: '2022-08-26 14:40:19', 61 | deletedAt: null 62 | }, 63 | ERROR_MESSAGE: 'Email must be unique' 64 | } 65 | }, 66 | RoleService: { 67 | createRole: { 68 | POSITIVE_CASE_INPUT: { 69 | name: 'Admin', 70 | description: 'Admin' 71 | }, 72 | SLUGIFY_OUTPUT: 'admin', 73 | POSITIVE_CASE_OUTPUT: { 74 | id: 1, 75 | name: 'Admin', 76 | description: 'Admin', 77 | slug: 'admin', 78 | createdAt: '2022-08-26 14:40:19', 79 | updatedAt: '2022-08-26 14:40:19', 80 | deletedAt: null 81 | }, 82 | CASE_NULL_ROLE_SLUG: null, 83 | CASE_EXIST_ROLE_SLUG: { 84 | id: 1, 85 | name: 'Admin', 86 | description: 'Admin', 87 | slug: 'admin', 88 | createdAt: '2022-08-26 14:40:19', 89 | updatedAt: '2022-08-26 14:40:19', 90 | deletedAt: null 91 | }, 92 | ERROR_MESSAGE: 'Role is exist' 93 | }, 94 | getRoles: { 95 | POSITIVE_CASE_OUTPUT: [ 96 | { 97 | id: 1, 98 | name: 'Admin', 99 | description: 'Admin', 100 | slug: 'admin', 101 | createdAt: '2022-08-26 14:40:19', 102 | updatedAt: '2022-08-26 14:40:19', 103 | deletedAt: null 104 | } 105 | ] 106 | } 107 | }, 108 | UserService: { 109 | createUser: { 110 | POSITIVE_CASE_INPUT: { 111 | email: 'user@mail.com', 112 | password: 'password', 113 | firstName: 'John', 114 | lastName: 'Doe', 115 | roleId: 1 116 | }, 117 | CASE_NULL_USER_EMAIL: null, 118 | CASE_EXIST_USER_EMAIL: { 119 | id: 1, 120 | email: 'user@mail.com', 121 | password: 122 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 123 | firstName: 'John', 124 | lastName: 'Doe', 125 | roleId: 1, 126 | createdAt: '2022-08-26 14:40:19', 127 | updatedAt: '2022-08-26 14:40:19', 128 | deletedAt: null 129 | }, 130 | BCRYPT_HASH_OUTPUT: 131 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 132 | POSITIVE_CASE_OUTPUT: { 133 | id: 1, 134 | email: 'user@mail.com', 135 | password: 136 | '$2b$05$bnaCGMUl/IYffmo9zku7c.AVDpdkJZPt.ZEIXsKULeQglPDyRU7Di', 137 | firstName: 'John', 138 | lastName: 'Doe', 139 | roleId: 1, 140 | createdAt: '2022-08-26 14:40:19', 141 | updatedAt: '2022-08-26 14:40:19', 142 | deletedAt: null 143 | }, 144 | ERROR_MESSAGE: 'Email must be unique' 145 | }, 146 | getUsers: { 147 | POSITIVE_CASE_OUTPUT: [ 148 | { 149 | id: 1, 150 | email: 'user@mail.com', 151 | firstName: 'John', 152 | lastName: 'Doe', 153 | roleId: 1 154 | } 155 | ] 156 | }, 157 | getUserDetail: { 158 | POSITIVE_CASE_INPUT: { 159 | userId: 1 160 | }, 161 | POSITIVE_CASE_OUTPUT: { 162 | id: 1, 163 | email: 'user@mail.com', 164 | firstName: 'John', 165 | lastName: 'Doe', 166 | role: { 167 | id: 1, 168 | name: 'Admin', 169 | description: 'Admin', 170 | slug: 'admin', 171 | createdAt: '2022-08-26 14:40:19', 172 | updatedAt: '2022-08-26 14:40:19', 173 | deletedAt: null 174 | } 175 | }, 176 | CASE_NULL_OUPUT: null, 177 | ERROR_MESSAGE: 'User not found' 178 | }, 179 | updateUser: { 180 | POSITIVE_CASE_INPUT: { 181 | userId: 1, 182 | payload: { 183 | firstName: 'John', 184 | lastName: 'Doe', 185 | roleId: 2 186 | } 187 | }, 188 | CASE_EXIST_DETAIL: { 189 | id: 1, 190 | email: 'user@mail.com', 191 | firstName: 'John', 192 | lastName: 'Doe', 193 | role: { 194 | id: 1, 195 | name: 'Admin', 196 | description: 'Admin', 197 | slug: 'admin', 198 | createdAt: '2022-08-26 14:40:19', 199 | updatedAt: '2022-08-26 14:40:19', 200 | deletedAt: null 201 | } 202 | }, 203 | CASE_NULL_DETAIL: null, 204 | POSITIVE_CASE_OUTPUT: true, 205 | ERROR_MESSAGE: 'User not found' 206 | }, 207 | deleteUser: { 208 | POSITIVE_CASE_INPUT: { 209 | userId: 1 210 | }, 211 | CASE_EXIST_DETAIL: { 212 | id: 1, 213 | email: 'user@mail.com', 214 | firstName: 'John', 215 | lastName: 'Doe', 216 | role: { 217 | id: 1, 218 | name: 'Admin', 219 | description: 'Admin', 220 | slug: 'admin', 221 | createdAt: '2022-08-26 14:40:19', 222 | updatedAt: '2022-08-26 14:40:19', 223 | deletedAt: null 224 | } 225 | }, 226 | CASE_NULL_DETAIL: null, 227 | POSITIVE_CASE_OUTPUT: true, 228 | ERROR_MESSAGE: 'User not found' 229 | } 230 | } 231 | }; 232 | 233 | export default mockResource; 234 | -------------------------------------------------------------------------------- /src/api/types/auth.ts: -------------------------------------------------------------------------------- 1 | export type LoginType = { 2 | email: string; 3 | password: string; 4 | }; 5 | 6 | export type SignUpType = { 7 | email: string; 8 | password: string; 9 | firstName: string; 10 | lastName?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/types/role.ts: -------------------------------------------------------------------------------- 1 | export type CreateRoleType = { 2 | name: string; 3 | description?: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/api/types/user.ts: -------------------------------------------------------------------------------- 1 | export type CreateUserType = { 2 | roleId?: number; 3 | firstName?: string; 4 | lastName?: string; 5 | email: string; 6 | password: string; 7 | }; 8 | 9 | export type UpdateUserType = { 10 | roleId?: number; 11 | firstName: string; 12 | lastName: string; 13 | }; 14 | -------------------------------------------------------------------------------- /src/config/appConfig.ts: -------------------------------------------------------------------------------- 1 | const AppConfig = { 2 | app: { 3 | name: process.env.APP_NAME, 4 | server: process.env.SERVER, 5 | isDevelopment: ['development', 'dev', 'local'].includes( 6 | process.env.SERVER 7 | ), 8 | port: parseInt(process.env.PORT, 10) || 3000, 9 | apiVersion: process.env.API_VERSION || 'v1', 10 | secret: process.env.SECRET || 'j!89nKO5as&Js' 11 | }, 12 | db: { 13 | host: process.env.DB_HOST, 14 | database: process.env.DB_DATABASE, 15 | username: process.env.DB_USERNAME, 16 | password: process.env.DB_PASSWORD, 17 | port: parseInt(process.env.DB_PORT, 10) || 5432, 18 | dialect: process.env.DB_DIALECT || 'postgres', 19 | timezone: process.env.DB_TIMEZONE || 'Asia/Jakarta', 20 | isLogging: process.env.DB_LOG === 'true' 21 | } 22 | }; 23 | 24 | export default Object.freeze(AppConfig); 25 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const ROLE = { 2 | ADMIN: 'admin' 3 | }; 4 | -------------------------------------------------------------------------------- /src/database/config.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Dialect } from 'sequelize'; 2 | import Logger from '../utils/logger'; 3 | import AppConfig from '../config/appConfig'; 4 | 5 | function customLog(msg: string) { 6 | Logger.debug(msg); 7 | } 8 | 9 | export const db: Sequelize = new Sequelize({ 10 | host: AppConfig.db.host, 11 | database: AppConfig.db.database, 12 | username: AppConfig.db.username, 13 | password: AppConfig.db.password, 14 | port: AppConfig.db.port, 15 | timezone: AppConfig.db.timezone, 16 | dialect: AppConfig.db.dialect as Dialect, 17 | logging: AppConfig.db.isLogging ? customLog : false 18 | }); 19 | -------------------------------------------------------------------------------- /src/database/sync.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import Role from '../api/models/Role'; 5 | import User from '../api/models/User'; 6 | 7 | const syncTables = () => Promise.all([User.sync(), Role.sync()]); 8 | 9 | syncTables() 10 | .then((result) => console.log(result)) 11 | .catch((error) => console.log(error)) 12 | .finally(() => process.exit()); 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import { Server } from 'net'; 5 | import { createServer } from './server'; 6 | import Logger from './utils/logger'; 7 | import AppConfig from './config/appConfig'; 8 | 9 | const PORT = AppConfig.app.port; 10 | 11 | function startServer(): Server { 12 | const app = createServer(); 13 | 14 | return app.listen(PORT, () => { 15 | Logger.debug( 16 | `App ${AppConfig.app.name} with api version ${AppConfig.app.apiVersion} is starting` 17 | ); 18 | Logger.debug(`App is listening on port ${PORT}`); 19 | }); 20 | } 21 | 22 | startServer(); 23 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as compression from 'compression'; 3 | import * as cors from 'cors'; 4 | import * as swaggerUi from 'swagger-ui-express'; 5 | import routesV1 from './api/routes/v1'; 6 | import MorganMiddleware from './api/middlewares/morgan'; 7 | import { Application } from 'express'; 8 | import AppConfig from './config/appConfig'; 9 | import { specs } from './utils/swagger'; 10 | import errorHandler from './api/middlewares/handlers/error'; 11 | 12 | export function createServer(): Application { 13 | const app = express(); 14 | const corsOption = { 15 | origin: '*', 16 | credentials: true 17 | }; 18 | 19 | app.use(express.urlencoded({ extended: false })); 20 | app.use(express.json()); 21 | app.use(cors(corsOption)); 22 | app.use(compression()); 23 | app.use(MorganMiddleware); 24 | app.use(`/api/${AppConfig.app.apiVersion}`, routesV1); 25 | 26 | if (AppConfig.app.isDevelopment) { 27 | app.use( 28 | `/docs/${AppConfig.app.apiVersion}`, 29 | swaggerUi.serve, 30 | swaggerUi.setup(specs) 31 | ); 32 | } 33 | 34 | app.use(errorHandler); 35 | 36 | return app; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export function slugify(text: string): string { 2 | return text 3 | .toLowerCase() 4 | .trim() 5 | .replace(/[^\w\s-]/g, '') 6 | .replace(/[\s_-]+/g, '-') 7 | .replace(/^-+|-+$/g, ''); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/jwt/index.ts: -------------------------------------------------------------------------------- 1 | import AppConfig from '../../config/appConfig'; 2 | import * as jwt from 'jsonwebtoken'; 3 | 4 | class JWT { 5 | signToken(userId: number, expires = '1d'): Promise { 6 | return new Promise((resolve, reject) => { 7 | jwt.sign( 8 | { 9 | id: userId, 10 | iat: Date.now() 11 | }, 12 | AppConfig.app.secret, 13 | { 14 | expiresIn: expires 15 | }, 16 | (err, token) => { 17 | if (err) { 18 | reject(err); 19 | } 20 | resolve(token); 21 | } 22 | ); 23 | }); 24 | } 25 | 26 | verifyToken(token: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | jwt.verify(token, AppConfig.app.secret, (err, decoded) => { 29 | if (err) { 30 | reject(err); 31 | } 32 | resolve(decoded); 33 | }); 34 | }); 35 | } 36 | } 37 | 38 | export default new JWT(); 39 | -------------------------------------------------------------------------------- /src/utils/logger/index.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | import AppConfig from '../../config/appConfig'; 3 | 4 | const levels = { 5 | error: 0, 6 | warn: 1, 7 | info: 2, 8 | http: 3, 9 | debug: 4 10 | }; 11 | 12 | const level = () => { 13 | return AppConfig.app.isDevelopment ? 'debug' : 'warn'; 14 | }; 15 | 16 | const colors = { 17 | error: 'red', 18 | warn: 'yellow', 19 | info: 'cyan', 20 | http: 'magenta', 21 | debug: 'green' 22 | }; 23 | 24 | winston.addColors(colors); 25 | 26 | const format = winston.format.combine( 27 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), 28 | winston.format.colorize({ all: true }), 29 | winston.format.printf( 30 | (info) => `${info.timestamp} ${info.level}: ${info.message}` 31 | ) 32 | ); 33 | 34 | const transports = [ 35 | new winston.transports.Console(), 36 | new winston.transports.File({ 37 | filename: 'logs/error.log', 38 | level: 'error' 39 | }), 40 | new winston.transports.File({ filename: 'logs/all.log' }) 41 | ]; 42 | 43 | const Logger = winston.createLogger({ 44 | level: level(), 45 | levels, 46 | format, 47 | transports 48 | }); 49 | 50 | export default Logger; 51 | -------------------------------------------------------------------------------- /src/utils/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import * as swaggerJsdoc from 'swagger-jsdoc'; 2 | import AppConfig from '../../config/appConfig'; 3 | 4 | const apiVersion = AppConfig.app.apiVersion; 5 | 6 | const options = { 7 | definition: { 8 | openapi: '3.0.0', 9 | info: { 10 | title: 'API Documentation', 11 | version: '1.0.0', 12 | description: 'API Documentation with swagger', 13 | termsOfService: 'http://example.com/terms/', 14 | contact: { 15 | name: 'API Support', 16 | url: 'http://www.example.com/support', 17 | email: 'support@example.com' 18 | }, 19 | license: { 20 | name: 'Apache 2.0', 21 | url: 'https://www.apache.org/licenses/LICENSE-2.0.html' 22 | } 23 | }, 24 | servers: [ 25 | { 26 | url: `/api/${apiVersion}`, 27 | description: `Server ${AppConfig.app.server}` 28 | } 29 | ], 30 | components: { 31 | securitySchemes: { 32 | bearerAuth: { 33 | type: 'http', 34 | scheme: 'bearer', 35 | bearerFormat: 'JWT' 36 | } 37 | }, 38 | responses: { 39 | '200': { 40 | description: 'OK', 41 | content: { 42 | 'application/json': {} 43 | } 44 | }, 45 | '400': { 46 | description: 'Bad Request' 47 | }, 48 | '401': { 49 | description: 'Unauthorized' 50 | }, 51 | '403': { 52 | descriptipn: 'Forbidden' 53 | }, 54 | '422': { 55 | description: 'Unprocessable entity' 56 | } 57 | } 58 | } 59 | }, 60 | apis: [`./docs/${apiVersion}/*.yaml`] 61 | }; 62 | 63 | export const specs = swaggerJsdoc(options); 64 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xc6c869DaF61F89f7228c42081856C84F4a0a7E40' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2017", 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "esModuleInterop": false, 9 | "strict": true, 10 | "baseUrl": ".", 11 | "typeRoots": ["node_modules/@types", "./src/@types/index.d.ts"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "src/**/__test__"] 15 | } 16 | --------------------------------------------------------------------------------