├── .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 | -
27 | About The Project
28 |
31 |
32 | -
33 | Getting Started
34 |
38 |
39 | - Usage
40 | - Contributing
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 |
--------------------------------------------------------------------------------