├── .commitlintrc.js ├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── docker-compose.yaml ├── images ├── user-structure.png └── user-swagger.png ├── libs ├── base │ ├── src │ │ ├── base.controller.ts │ │ ├── base.dto.ts │ │ ├── base.entity.ts │ │ ├── base.service.ts │ │ ├── base.swagger.ts │ │ └── index.ts │ └── tsconfig.lib.json ├── constants │ ├── src │ │ └── index.ts │ └── tsconfig.lib.json ├── crypto │ ├── src │ │ ├── crypto.module.ts │ │ ├── crypto.service.ts │ │ └── index.ts │ └── tsconfig.lib.json ├── database │ ├── src │ │ ├── database.module.ts │ │ └── index.ts │ └── tsconfig.lib.json ├── decorators │ ├── src │ │ ├── auth-admin.decorator.ts │ │ ├── index.ts │ │ └── user.decorator.ts │ └── tsconfig.lib.json ├── enums │ ├── src │ │ ├── index.ts │ │ └── role.enum.ts │ └── tsconfig.lib.json ├── filters │ ├── src │ │ ├── http-exception.filter.ts │ │ ├── index.ts │ │ └── typeorm-exception.filter.ts │ └── tsconfig.lib.json ├── guards │ ├── src │ │ ├── index.ts │ │ └── role.guard.ts │ └── tsconfig.lib.json ├── helpers │ ├── src │ │ ├── index.ts │ │ ├── paginationToQuery.ts │ │ ├── queryBuilder.ts │ │ └── setCookieRFToken.ts │ └── tsconfig.lib.json ├── interceptors │ ├── src │ │ ├── index.ts │ │ └── response-transform.interceptor.ts │ └── tsconfig.lib.json ├── interfaces │ ├── src │ │ ├── format-response.interface.ts │ │ ├── index.ts │ │ ├── pagination.interface.ts │ │ └── redis.interface.ts │ └── tsconfig.lib.json ├── jwt │ ├── src │ │ ├── index.ts │ │ ├── jwt.interface.ts │ │ ├── jwt.module.ts │ │ └── jwt.service.ts │ └── tsconfig.lib.json └── redis │ ├── src │ ├── index.ts │ ├── redis.module.ts │ └── redis.service.ts │ └── tsconfig.lib.json ├── nest-cli.json ├── ormconfig.ts ├── package.json ├── patches └── @nestjs+schematics+9.2.0.patch ├── src ├── apis │ ├── admin │ │ ├── admin.module.ts │ │ ├── controllers │ │ │ └── admin.controller.ts │ │ ├── dto │ │ │ ├── create-admin.dto.ts │ │ │ └── update-admin.dto.ts │ │ ├── entities │ │ │ └── admin.entity.ts │ │ ├── services │ │ │ └── admin.service.ts │ │ └── test │ │ │ ├── admin.controller.spec.ts │ │ │ └── admin.service.spec.ts │ ├── api.module.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.swagger.ts │ │ ├── controllers │ │ │ ├── auth.admin.controller.ts │ │ │ └── auth.base.controller.ts │ │ ├── dtos │ │ │ ├── change-password.dto.ts │ │ │ ├── login-admin.dto.ts │ │ │ └── login.dto.ts │ │ ├── interfaces │ │ │ └── auth.interface.ts │ │ ├── services │ │ │ └── auth.service.ts │ │ └── strategies │ │ │ ├── jwt │ │ │ └── admin.jwt.strategy.ts │ │ │ └── local │ │ │ └── admin.local.strategy.ts │ ├── chat │ │ ├── chat.gateway.ts │ │ ├── chat.module.ts │ │ ├── controllers │ │ │ ├── message.controller.ts │ │ │ └── room.controller.ts │ │ ├── dto │ │ │ ├── chat-message.dto.ts │ │ │ ├── create-message.dto.ts │ │ │ ├── create-room.dto.ts │ │ │ └── update-room.dto.ts │ │ ├── entities │ │ │ ├── message.entity.ts │ │ │ └── room.entity.ts │ │ └── services │ │ │ ├── message.service.ts │ │ │ └── room.service.ts │ └── upload │ │ ├── upload.controller.ts │ │ ├── upload.module.ts │ │ └── upload.service.ts ├── app │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.provider.ts │ ├── app.service.ts │ └── app.swagger.ts └── main.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .husky 4 | .vscode 5 | patches 6 | uploads 7 | .commitlintrc.js 8 | .dockerignore 9 | .env.* 10 | .eslintrc.js 11 | .gitignore 12 | .prettierrc 13 | Dockerfile 14 | docker-compose.yaml 15 | README.md -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3000 3 | DB_HOST=localhost 4 | DB_PORT=5432 5 | DB_USERNAME= 6 | DB_PASSWORD= 7 | DB_NAME= 8 | REDIS_HOST=localhost 9 | REDIS_PORT=6379 10 | REDIS_DB=2 11 | REDIS_PASSWORD= 12 | SECRET_JWT=secret 13 | SECRET_KEY=secret 14 | SECRET_KEY_IV=secret -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module' 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin', '@darraghor/nestjs-typed'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | 'plugin:@darraghor/nestjs-typed/recommended' 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: false 18 | }, 19 | ignorePatterns: ['.eslintrc.js', '*.spec.ts', '*.e2e-spec.ts'], 20 | rules: { 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 23 | '@typescript-eslint/ban-types': 'error', 24 | '@typescript-eslint/no-empty-interface': 'error', 25 | '@typescript-eslint/no-var-requires': 'warn', 26 | '@typescript-eslint/no-non-null-assertion': 'error', 27 | '@typescript-eslint/prefer-optional-chain': 'warn', 28 | '@typescript-eslint/naming-convention': [ 29 | 'error', 30 | { 31 | selector: 'accessor', 32 | format: ['camelCase'] 33 | }, 34 | { 35 | selector: 'property', 36 | format: ['camelCase', 'UPPER_CASE'] 37 | }, 38 | { 39 | selector: 'method', 40 | format: ['camelCase'] 41 | }, 42 | { 43 | selector: 'variable', 44 | format: ['camelCase', 'PascalCase'] 45 | }, 46 | { 47 | selector: 'function', 48 | format: ['PascalCase', 'camelCase'] 49 | }, 50 | { 51 | selector: 'parameter', 52 | format: ['camelCase'], 53 | leadingUnderscore: 'allow' 54 | }, 55 | { 56 | selector: 'typeLike', 57 | format: ['PascalCase'] 58 | }, 59 | { 60 | selector: 'enumMember', 61 | format: ['UPPER_CASE'] 62 | } 63 | ], 64 | '@darraghor/nestjs-typed/all-properties-have-explicit-defined': 'error', 65 | '@darraghor/nestjs-typed/validate-nested-of-array-should-set-each': 'error', 66 | '@darraghor/nestjs-typed/param-decorator-name-matches-route-param': 'error', 67 | '@darraghor/nestjs-typed/provided-injected-should-match-factory-parameters': 'error', 68 | '@darraghor/nestjs-typed/controllers-should-supply-api-tags': 'warn', 69 | '@darraghor/nestjs-typed/api-enum-property-best-practices': 'warn', 70 | '@darraghor/nestjs-typed/api-property-returning-array-should-set-array': 'warn', 71 | '@darraghor/nestjs-typed/api-method-should-specify-api-response': 'off', 72 | '@darraghor/nestjs-typed/should-specify-forbid-unknown-values': 'off' 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env.production 38 | .env.development 39 | .env.test 40 | .env 41 | 42 | uploads 43 | .docker -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "css", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "proseWrap": "always", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleAttributePerLine": false, 14 | "singleQuote": true, 15 | "tabWidth": 4, 16 | "trailingComma": "none", 17 | "useTabs": true, 18 | "vueIndentScriptAndStyle": false, 19 | "printWidth": 100 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "material-icon-theme.activeIconPack": "nest", 3 | "material-icon-theme.files.associations": { 4 | "main.ts": "nest" 5 | } 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as build 2 | 3 | WORKDIR /app 4 | 5 | #Install @nestjs/cli 6 | RUN yarn global add @nestjs/cli 7 | 8 | #Install dependencies 9 | COPY package.json . 10 | RUN yarn 11 | 12 | #Build 13 | COPY . . 14 | RUN yarn build 15 | 16 | FROM node:18-alpine as production 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=build /app/node_modules /app/node_modules 21 | COPY --from=build /app/dist /app/dist 22 | COPY --from=build /app/package.json /app/package.json 23 | 24 | EXPOSE 3000 25 | 26 | CMD ["yarn", "start:prod"] 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 | 9 | ## Description 10 | 11 | A NestJS source for fast creating CRUD modules. Using OOP to reuse inheritable classes 12 | 13 | ## Installation 14 | 15 | ```bash 16 | $ yarn install 17 | ``` 18 | 19 | ## Config environment 20 | ```bash 21 | $ cp .env.example .env.development 22 | ``` 23 | 24 | ```bash 25 | NODE_ENV=development | production | test 26 | PORT= App Port 27 | DB_HOST= Database Host 28 | DB_PORT= Database Port 29 | DB_USERNAME= Database Username 30 | DB_PASSWORD= Database Password 31 | DB_NAME= Database Name 32 | REDIS_HOST= Redis Host 33 | REDIS_PORT= Redis Port 34 | REDIS_DB= Redis DB 35 | REDIS_PASSWORD= Redis Password 36 | SECRET_JWT= What ever 37 | SECRET_KEY= What ever 38 | SECRET_KEY_IV= What ever 39 | ``` 40 | 41 | ## Running the app 42 | 43 | ```bash 44 | # development 45 | $ yarn run start 46 | 47 | # watch mode 48 | $ yarn run start:dev 49 | 50 | # production mode 51 | $ yarn run start:prod 52 | ``` 53 | ## Docker 54 | ```bash 55 | $ docker compose up -d --build 56 | ``` 57 | 58 | ## Generate CRUD Module 59 | I have customized @nestjs/schematics that will generate follow my own template. 60 | ```bash 61 | $ nest g res module-name 62 | ``` 63 | Choose Restful and Yes when be asked for CRUD 64 | 65 | ## Structure 66 | This is my customized template 67 | ```bash 68 | module-name 69 | ├── controllers 70 | ├── dto 71 | ├── entities 72 | ├── services 73 | └── test 74 | ``` 75 | 76 | ## Base classes 77 | ### BaseEntity 78 | ```typescript 79 | export class BaseEntity extends TypeormBaseEntity { 80 | @PrimaryGeneratedColumn('uuid') 81 | id!: string; 82 | 83 | @CreateDateColumn({ name: 'created_at' }) 84 | createdAt!: Date; 85 | 86 | @UpdateDateColumn({ name: 'updated_at' }) 87 | updatedAt!: Date; 88 | 89 | @DeleteDateColumn({ name: 'deleted_at' }) 90 | @Exclude() 91 | @ApiHideProperty() 92 | deletedAt!: Date; 93 | } 94 | ``` 95 | This class extends BaseEntity of typeorm. Already includes id and timestamp, that are necessary for all entities 96 | ### BaseService 97 | ```typescript 98 | export abstract class BaseService { 99 | abstract name: string; 100 | 101 | constructor(public readonly repo: Repository) {} 102 | 103 | async getAll(): Promise { 104 | ... 105 | } 106 | 107 | async getAllWithPagination(): Promise<[Entity[], number]> { 108 | ... 109 | } 110 | 111 | async getOne(): Promise { 112 | ... 113 | } 114 | 115 | async getOneById(): Promise { 116 | ... 117 | } 118 | 119 | async getOneOrFail(): Promise { 120 | ... 121 | } 122 | 123 | async getOneByIdOrFail(): Promise { 124 | ... 125 | } 126 | 127 | async create(): Promise { 128 | ... 129 | } 130 | 131 | async createMany(): Promise { 132 | ... 133 | } 134 | 135 | async update() { 136 | ... 137 | } 138 | 139 | async updateBy() { 140 | ... 141 | } 142 | 143 | async updateById() { 144 | ... 145 | } 146 | 147 | async deleteBy() { 148 | ... 149 | } 150 | 151 | async deleteById() { 152 | ... 153 | } 154 | 155 | async softDelete() { 156 | ... 157 | } 158 | 159 | async softDeleteById() { 160 | ... 161 | } 162 | } 163 | ``` 164 | This class include all methods that each module needed. 165 | - create, createMany 166 | - getAll, getOne, getWithPagination 167 | - update 168 | - delete 169 | - softDelete 170 | ### BaseController 171 | ```typescript 172 | export function BaseController($ref: any, name?: string) { 173 | abstract class Controller { 174 | abstract relations: string[]; 175 | 176 | constructor(public readonly service: BaseService) {} 177 | 178 | @Post('create') 179 | @ApiCreate($ref, name) 180 | create(@Body() body): Promise { 181 | return this.service.create(body); 182 | } 183 | 184 | @Get('all') 185 | @ApiGetAll($ref, name) 186 | getAll(@Query() query: PaginationDto): Promise<[Entity[], number]> { 187 | return this.service.getAllWithPagination( 188 | query, 189 | {}, 190 | //@ts-ignore 191 | { createdAt: 'DESC' }, 192 | ...this.relations 193 | ); 194 | } 195 | 196 | @Get('detail/:id') 197 | @ApiGetDetail($ref, name) 198 | getDetail(@Param('id') id: string): Promise { 199 | return this.service.getOneByIdOrFail(id, ...this.relations); 200 | } 201 | 202 | @Patch('update/:id') 203 | @ApiUpdate($ref, name) 204 | update(@Param('id') id: string, @Body() body): Promise { 205 | return this.service.updateById(id, body); 206 | } 207 | 208 | @Delete('delete/:id') 209 | @ApiDelete($ref, name) 210 | delete(@Param('id') id: string): Promise { 211 | return this.service.softDeleteById(id); 212 | } 213 | } 214 | 215 | return Controller; 216 | } 217 | ``` 218 | BaseController just call methods of BaseService. 219 | ## Git convention 220 | My project's convention follow on Angular git convention 221 | 222 | ## Example 223 | In this section, i will tutor you how to fast generate a CRUD Module. Example for User module 224 | 225 | ### Generate 226 | ```bash 227 | $ nest g res apis/user 228 | ``` 229 | 230 | ```bash 231 | ? Which project would you like to generate to? (Use arrow keys) 232 | ❯ src [ Default ] <- Choose this 233 | jwt 234 | crypto 235 | redis 236 | database 237 | base 238 | decorators 239 | (Move up and down to reveal more choices) 240 | ``` 241 | ```bash 242 | ? What transport layer do you use? (Use arrow keys) 243 | ❯ REST API <- Enter right here 244 | GraphQL (code first) 245 | GraphQL (schema first) 246 | Microservice (non-HTTP) 247 | WebSockets 248 | ``` 249 | ```bash 250 | ? Would you like to generate CRUD entry points? (Y/n)y <- Type Y for yes 251 | ``` 252 | Look at terminal, you will see this 253 | ```bash 254 | CREATE src/apis/user/user.module.ts (438 bytes) 255 | CREATE src/apis/user/controllers/user.controller.ts (950 bytes) 256 | CREATE src/apis/user/dto/create-user.dto.ts (30 bytes) 257 | CREATE src/apis/user/dto/update-user.dto.ts (164 bytes) 258 | CREATE src/apis/user/entities/user.entity.ts (159 bytes) 259 | CREATE src/apis/user/services/user.service.ts (461 bytes) 260 | CREATE src/apis/user/test/user.controller.spec.ts (579 bytes) 261 | CREATE src/apis/user/test/user.service.spec.ts (456 bytes) 262 | UPDATE src/apis/api.module.ts (409 bytes) 263 | ``` 264 | That's all, an CRUD Module for User has been created. 265 |

266 | Nest Logo 267 |

268 | 269 | ### UserModule 270 | ```typescript 271 | @Module({ 272 | imports: [TypeOrmModule.forFeature([UserEntity])], 273 | controllers: [UserController], 274 | providers: [UserService], 275 | exports: [UserService] 276 | }) 277 | export class UserModule {} 278 | ``` 279 | This module will automatic import TypeormModule and exports the UserService 280 | ### UserEntity 281 | ```typescript 282 | import { Entity } from 'typeorm'; 283 | import { BaseEntity } from '@app/base/base.entity'; 284 | 285 | @Entity({ name: 'user' }) 286 | export class UserEntity extends BaseEntity {} 287 | ``` 288 | UserEntity just extends BaseEntity and name the table as the name of module 289 | ### UserService 290 | ```typescript 291 | @Injectable() 292 | export class UserService extends BaseService { 293 | name = 'User'; 294 | 295 | constructor( 296 | @InjectRepository(UserEntity) 297 | private readonly userRepo: Repository 298 | ) { 299 | super(userRepo); 300 | } 301 | } 302 | ``` 303 | UserService extends BaseService with generic UserEntity. BaseService have an abstract property is name, that will be module's name. Then inject repository into constructor and call super method. 304 | ### UserController 305 | ```typescript 306 | @Controller('user') 307 | @ApiTags('User API') 308 | export class UserController extends BaseController(UserEntity, 'user') { 309 | relations = []; 310 | 311 | constructor(private readonly userService: UserService) { 312 | super(userService); 313 | } 314 | 315 | @Post() 316 | @ApiCreate(UserEntity, 'user') 317 | create(@Body() body: CreateUserDto) { 318 | return super.create(body); 319 | } 320 | 321 | @Patch(':id') 322 | @ApiUpdate(UserEntity, 'user') 323 | update(@Param('id') id: string, @Body() body: UpdateUserDto) { 324 | return super.update(id, body); 325 | } 326 | } 327 | ``` 328 | UserController extends BaseController, that return a class have 5 methods CRUD. 329 | Actually in BaseController already have 2 methods create and update. But it doesn't recognize your dto classes, so i have to override this 2 methods and pass dto classes to parameter 330 | 331 | Now, let check it out 332 | ```bash 333 | $ yarn start:dev 334 | ``` 335 | 336 | Go to [Swagger](http://localhost:3000/docs) 337 |

338 | Nest Logo 339 |

-------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | server: 5 | container_name: nest_server 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: 10 | - 3000:3000 11 | env_file: ./.env 12 | volumes: 13 | - ./uploads:/app/uploads 14 | depends_on: 15 | - db 16 | - redis 17 | db: 18 | container_name: nest_db 19 | image: postgres:14.1-alpine 20 | restart: always 21 | env_file: ./.env 22 | environment: 23 | - POSTGRES_USER=$DB_USERNAME 24 | - POSTGRES_PASSWORD=$DB_PASSWORD 25 | - POSTGRES_DB=$DB_NAME 26 | ports: 27 | - '5433:5432' 28 | volumes: 29 | - ./.docker/db:/var/lib/postgresql/data 30 | redis: 31 | container_name: nest_redis 32 | image: redis:6.2-alpine 33 | restart: unless-stopped 34 | env_file: ./.env 35 | ports: 36 | - "6377:6379" 37 | command: redis-server --save 20 1 --loglevel warning --requirepass $REDIS_PASSWORD 38 | volumes: 39 | - './.docker/redis:/data' 40 | -------------------------------------------------------------------------------- /images/user-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dobafullstack/nestjs-crud/21c8d01455694a72f8d8b3ee6142b377ff75aac1/images/user-structure.png -------------------------------------------------------------------------------- /images/user-swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dobafullstack/nestjs-crud/21c8d01455694a72f8d8b3ee6142b377ff75aac1/images/user-swagger.png -------------------------------------------------------------------------------- /libs/base/src/base.controller.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { Body, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; 3 | import { PaginationDto } from './base.dto'; 4 | import { BaseEntity } from './base.entity'; 5 | import { BaseService } from './base.service'; 6 | import { ApiCreate, ApiDelete, ApiGetAll, ApiGetDetail, ApiUpdate } from './base.swagger'; 7 | 8 | export function BaseController($ref: any, name?: string) { 9 | abstract class Controller { 10 | abstract relations: string[]; 11 | 12 | constructor(public readonly service: BaseService) {} 13 | 14 | @Post('create') 15 | @ApiCreate($ref, name) 16 | create(@Body() body): Promise { 17 | return this.service.create(body); 18 | } 19 | 20 | @Get('all') 21 | @ApiGetAll($ref, name) 22 | getAll(@Query() query: PaginationDto): Promise<[Entity[], number]> { 23 | return this.service.getAllWithPagination( 24 | query, 25 | {}, 26 | //@ts-ignore 27 | { createdAt: 'DESC' }, 28 | ...this.relations 29 | ); 30 | } 31 | 32 | @Get('detail/:id') 33 | @ApiGetDetail($ref, name) 34 | getDetail(@Param('id') id: string): Promise { 35 | return this.service.getOneByIdOrFail(id, ...this.relations); 36 | } 37 | 38 | @Patch('update/:id') 39 | @ApiUpdate($ref, name) 40 | update(@Param('id') id: string, @Body() body): Promise { 41 | return this.service.updateById(id, body); 42 | } 43 | 44 | @Delete('delete/:id') 45 | @ApiDelete($ref, name) 46 | delete(@Param('id') id: string): Promise { 47 | return this.service.softDeleteById(id); 48 | } 49 | } 50 | 51 | return Controller; 52 | } 53 | -------------------------------------------------------------------------------- /libs/base/src/base.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumberString, IsOptional } from 'class-validator'; 2 | 3 | export class PaginationDto { 4 | @IsOptional() 5 | @IsNumberString() 6 | limit?: string; 7 | 8 | @IsOptional() 9 | @IsNumberString() 10 | page?: string; 11 | } 12 | -------------------------------------------------------------------------------- /libs/base/src/base.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiHideProperty } from '@nestjs/swagger'; 2 | import { Exclude } from 'class-transformer'; 3 | import { 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | PrimaryGeneratedColumn, 7 | BaseEntity as TypeormBaseEntity, 8 | UpdateDateColumn 9 | } from 'typeorm'; 10 | 11 | export class BaseEntity extends TypeormBaseEntity { 12 | @PrimaryGeneratedColumn('uuid') 13 | id!: string; 14 | 15 | @CreateDateColumn({ name: 'created_at' }) 16 | createdAt!: Date; 17 | 18 | @UpdateDateColumn({ name: 'updated_at' }) 19 | updatedAt!: Date; 20 | 21 | @DeleteDateColumn({ name: 'deleted_at' }) 22 | @Exclude() 23 | @ApiHideProperty() 24 | deletedAt!: Date; 25 | } 26 | -------------------------------------------------------------------------------- /libs/base/src/base.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { getQueryBuilder } from '@app/helpers/queryBuilder'; 3 | import { NotFoundException } from '@nestjs/common'; 4 | import { DeepPartial, FindOptionsOrder, FindOptionsWhere, Repository } from 'typeorm'; 5 | import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; 6 | import { PaginationDto } from './base.dto'; 7 | import { BaseEntity } from './base.entity'; 8 | 9 | export abstract class BaseService { 10 | abstract name: string; 11 | 12 | constructor(public readonly repo: Repository) {} 13 | 14 | async getAll( 15 | where?: FindOptionsWhere | FindOptionsWhere[], 16 | ...relations: string[] 17 | ): Promise { 18 | return this.repo.find({ where, relations }); 19 | } 20 | 21 | async getAllWithPagination( 22 | query: PaginationDto, 23 | where?: FindOptionsWhere | FindOptionsWhere[], 24 | order?: FindOptionsOrder, 25 | ...relations: string[] 26 | ): Promise<[Entity[], number]> { 27 | const queryBuilder = getQueryBuilder(this.repo, query, where, order, ...relations); 28 | return queryBuilder.getManyAndCount(); 29 | } 30 | 31 | async getOne( 32 | where?: FindOptionsWhere | FindOptionsWhere[], 33 | ...relations: string[] 34 | ): Promise { 35 | return this.repo.findOne({ where, relations }); 36 | } 37 | 38 | async getOneById(id: string, ...relations: string[]): Promise { 39 | //@ts-ignore 40 | return this.repo.findOne({ where: { id }, relations }); 41 | } 42 | 43 | async getOneOrFail( 44 | where?: FindOptionsWhere | FindOptionsWhere[] 45 | ): Promise { 46 | const entity = await this.repo.findOne({ where }); 47 | if (!entity) { 48 | const errorMessage = `${this.name} not found`; 49 | throw new NotFoundException(errorMessage); 50 | } 51 | return entity; 52 | } 53 | 54 | async getOneByIdOrFail(id: string, ...relations: string[]): Promise { 55 | //@ts-ignore 56 | const entity = await this.repo.findOne({ where: { id }, relations }); 57 | if (!entity) { 58 | const errorMessage = `${this.name} not found`; 59 | throw new NotFoundException(errorMessage); 60 | } 61 | return entity; 62 | } 63 | 64 | async create(data: DeepPartial): Promise { 65 | return this.repo.create(data).save(); 66 | } 67 | 68 | async createMany(data: DeepPartial[]): Promise { 69 | const result: Entity[] = []; 70 | const newEntities = this.repo.create(data); 71 | for (let i = 0; i < newEntities.length; i++) { 72 | const newEntity = await newEntities[i].save(); 73 | result.push(newEntity); 74 | } 75 | return result; 76 | } 77 | 78 | async update(entity: Entity, data: QueryDeepPartialEntity) { 79 | const keys = Object.keys(data); 80 | for (let i = 0; i < keys.length; i++) { 81 | const key = keys[i]; 82 | entity[key] = data[key]; 83 | } 84 | return entity.save(); 85 | } 86 | 87 | async updateBy( 88 | where: FindOptionsWhere | FindOptionsWhere[], 89 | data: QueryDeepPartialEntity 90 | ) { 91 | const entity = await this.getOneOrFail(where); 92 | return this.update(entity, data); 93 | } 94 | 95 | async updateById(id: string, data: QueryDeepPartialEntity) { 96 | const entity = await this.getOneByIdOrFail(id); 97 | return this.update(entity, data); 98 | } 99 | 100 | async deleteBy(where: FindOptionsWhere | FindOptionsWhere[]) { 101 | const entity = await this.getOneOrFail(where); 102 | return this.repo.remove(entity); 103 | } 104 | 105 | async deleteById(id: string) { 106 | const entity = await this.getOneByIdOrFail(id); 107 | return this.repo.remove(entity); 108 | } 109 | 110 | async softDelete(where: FindOptionsWhere | FindOptionsWhere[]) { 111 | const entity = await this.getOneOrFail(where); 112 | return this.repo.softRemove(entity); 113 | } 114 | 115 | async softDeleteById(id: string) { 116 | const entity = await this.getOneByIdOrFail(id); 117 | return this.repo.softRemove(entity); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /libs/base/src/base.swagger.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { ApiCreatedResponse, ApiOkResponse, ApiOperation, getSchemaPath } from '@nestjs/swagger'; 3 | import { 4 | ReferenceObject, 5 | SchemaObject 6 | } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; 7 | 8 | export function OkResponse($ref: any, isPagination = false, data?: SchemaObject | ReferenceObject) { 9 | const properties: Record = { 10 | statusCode: { example: 200 }, 11 | success: { example: true } 12 | }; 13 | if ($ref) { 14 | let metadata: SchemaObject | ReferenceObject | undefined; 15 | let refData: SchemaObject | ReferenceObject = { $ref: getSchemaPath($ref) }; 16 | if (isPagination) { 17 | refData = { 18 | type: 'array', 19 | items: { $ref: getSchemaPath($ref) } 20 | }; 21 | metadata = { 22 | example: { 23 | limit: 0, 24 | page: 0, 25 | totalItems: 0, 26 | totalPages: 0 27 | } 28 | }; 29 | } 30 | Object.assign(properties, { data: refData }); 31 | if (metadata) { 32 | Object.assign(properties, { metadata }); 33 | } 34 | } 35 | 36 | if (data) { 37 | Object.assign(properties, { data }); 38 | } 39 | 40 | return ApiOkResponse({ 41 | schema: { 42 | properties 43 | } 44 | }); 45 | } 46 | 47 | export function CreatedResponse($ref: any) { 48 | return ApiCreatedResponse({ 49 | schema: { 50 | properties: { 51 | status: { example: 200 }, 52 | data: { 53 | properties: { 54 | result: { 55 | type: 'array', 56 | items: { 57 | $ref: getSchemaPath($ref) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }); 65 | } 66 | 67 | export function ApiCreate($ref: any, name?: string) { 68 | return applyDecorators(ApiOperation({ summary: 'Create new ' + name }), CreatedResponse($ref)); 69 | } 70 | 71 | export function ApiGetAll($ref: any, name?: string) { 72 | return applyDecorators(ApiOperation({ summary: 'Get all ' + name }), OkResponse($ref, true)); 73 | } 74 | 75 | export function ApiGetDetail($ref: any, name?: string) { 76 | return applyDecorators(ApiOperation({ summary: 'Get detail ' + name }), OkResponse($ref)); 77 | } 78 | 79 | export function ApiUpdate($ref: any, name?: string) { 80 | return applyDecorators(ApiOperation({ summary: 'Update ' + name }), OkResponse($ref)); 81 | } 82 | 83 | export function ApiDelete($ref: any, name?: string) { 84 | return applyDecorators(ApiOperation({ summary: 'Delete ' + name }), OkResponse($ref)); 85 | } 86 | -------------------------------------------------------------------------------- /libs/base/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.controller'; 2 | export * from './base.dto'; 3 | export * from './base.entity'; 4 | export * from './base.service'; 5 | export * from './base.swagger'; 6 | -------------------------------------------------------------------------------- /libs/base/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/base" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/constants/src/index.ts: -------------------------------------------------------------------------------- 1 | export const MetadataKey = { 2 | REDIS: 'redis', 3 | ROLE: 'role' 4 | }; 5 | 6 | export const StrategyKey = { 7 | LOCAL: { 8 | ADMIN: 'local_admin' 9 | }, 10 | JWT: { 11 | ADMIN: 'jwt_admin' 12 | } 13 | }; 14 | 15 | export const TokenExpires = { 16 | accessToken: '15d', 17 | refreshToken: '30d', 18 | redisAccessToken: 60 * 60 * 24 * 15, 19 | redisRefreshToken: 60 * 60 * 24 * 30 20 | }; 21 | -------------------------------------------------------------------------------- /libs/constants/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/constants" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/crypto/src/crypto.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { CryptoService } from './crypto.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [CryptoService], 7 | exports: [CryptoService] 8 | }) 9 | export class CryptoModule {} 10 | -------------------------------------------------------------------------------- /libs/crypto/src/crypto.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotAcceptableException } from '@nestjs/common'; 2 | import { createCipheriv, createDecipheriv, createHash } from 'crypto'; 3 | 4 | @Injectable() 5 | export class CryptoService { 6 | get() { 7 | const secretKey = process.env.SECRET_KEY as string; 8 | const secretIV = process.env.SECRET_KEY_IV as string; 9 | const key = createHash('sha512').update(secretKey).digest('hex').substring(0, 32); 10 | const encryptionIV = createHash('sha512').update(secretIV).digest('hex').substring(0, 16); 11 | return { 12 | key, 13 | encryptionIV 14 | }; 15 | } 16 | 17 | encryptData(data: string): string { 18 | const { key, encryptionIV } = this.get(); 19 | const cipher = createCipheriv('aes256', key, encryptionIV); 20 | return Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex')).toString( 21 | 'base64' 22 | ); // Encrypts data and converts to hex and base64 23 | } 24 | 25 | decryptData(token: string) { 26 | const buff = Buffer.from(token, 'base64'); 27 | const { key, encryptionIV } = this.get(); 28 | const decipher = createDecipheriv('aes256', key, encryptionIV); 29 | try { 30 | // Decrypts data and converts to utf8 31 | const decryptData = 32 | decipher.update(buff.toString('utf8'), 'hex', 'utf8') + decipher.final('utf8'); 33 | return decryptData; 34 | } catch (error) { 35 | throw new NotAcceptableException('Not acceptable'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /libs/crypto/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto.module'; 2 | export * from './crypto.service'; 3 | -------------------------------------------------------------------------------- /libs/crypto/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/crypto" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/database/src/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { options } from 'ormconfig'; 5 | 6 | @Module({ 7 | imports: [ 8 | TypeOrmModule.forRootAsync({ 9 | inject: [ConfigService], 10 | useFactory: () => ({ 11 | ...options, 12 | autoLoadEntities: true 13 | }) 14 | }) 15 | ] 16 | }) 17 | export class DatabaseModule {} 18 | -------------------------------------------------------------------------------- /libs/database/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database.module'; 2 | -------------------------------------------------------------------------------- /libs/database/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/database" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/decorators/src/auth-admin.decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataKey, StrategyKey } from '@app/constants'; 2 | import { Roles } from '@app/enums'; 3 | import { RoleGuard } from '@app/guards'; 4 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common'; 5 | import { AuthGuard } from '@nestjs/passport'; 6 | import { ApiBearerAuth } from '@nestjs/swagger'; 7 | 8 | export const AuthAdmin = (...roles: Roles[]) => { 9 | return applyDecorators( 10 | SetMetadata(MetadataKey.ROLE, roles), 11 | UseGuards(AuthGuard(StrategyKey.JWT.ADMIN)), 12 | UseGuards(RoleGuard), 13 | ApiBearerAuth() 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /libs/decorators/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-admin.decorator'; 2 | export * from './user.decorator'; 3 | -------------------------------------------------------------------------------- /libs/decorators/src/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | 4 | export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user; 7 | }); 8 | -------------------------------------------------------------------------------- /libs/decorators/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/decorators" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/enums/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './role.enum'; 2 | -------------------------------------------------------------------------------- /libs/enums/src/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Roles { 2 | ADMIN = 'admin', 3 | MANAGER = 'manager', 4 | STAFF = 'staff' 5 | } 6 | -------------------------------------------------------------------------------- /libs/enums/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/enums" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/filters/src/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | 4 | @Catch(HttpException) 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | catch(exception: HttpException, host: ArgumentsHost) { 7 | const ctx = host.switchToHttp(); 8 | const response = ctx.getResponse(); 9 | 10 | const statusCode = exception.getStatus(); 11 | const errors = exception.getResponse(); 12 | const exceptionName = exception.name; 13 | 14 | if (exceptionName === 'BadRequestException') { 15 | return response.status(400).json({ 16 | statusCode: 400, 17 | success: false, 18 | errors 19 | }); 20 | } 21 | 22 | return response.status(statusCode).json({ 23 | statusCode, 24 | success: false, 25 | errors: { 26 | message: errors['message'] 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libs/filters/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-exception.filter'; 2 | export * from './typeorm-exception.filter'; 3 | -------------------------------------------------------------------------------- /libs/filters/src/typeorm-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { TypeORMError } from 'typeorm'; 4 | 5 | @Catch(TypeORMError) 6 | export class TypeormExceptionFilter implements ExceptionFilter { 7 | catch(exception: TypeORMError, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | let statusCode = 500; 11 | const code = exception['code']; 12 | const message = exception['message']; 13 | const errors = { 14 | code, 15 | message 16 | }; 17 | if (code === '23505') { 18 | errors['message'] = exception['detail']; 19 | statusCode = 409; 20 | } 21 | return response.status(statusCode).json({ 22 | statusCode, 23 | success: false, 24 | errors 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/filters/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/filters" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/guards/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './role.guard'; 2 | -------------------------------------------------------------------------------- /libs/guards/src/role.guard.ts: -------------------------------------------------------------------------------- 1 | import { MetadataKey } from '@app/constants'; 2 | import { Roles } from '@app/enums'; 3 | import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; 4 | import { Reflector } from '@nestjs/core'; 5 | import { Request } from 'express'; 6 | import { Observable } from 'rxjs'; 7 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 8 | import { AdminService } from 'src/apis/admin/services/admin.service'; 9 | 10 | @Injectable() 11 | export class RoleGuard implements CanActivate { 12 | constructor(private reflector: Reflector, private adminService: AdminService) {} 13 | 14 | canActivate(context: ExecutionContext): boolean | Promise | Observable { 15 | const requiredRoles = this.reflector.getAllAndOverride(MetadataKey.ROLE, [ 16 | context.getHandler(), 17 | context.getClass() 18 | ]); 19 | if (!requiredRoles || requiredRoles.length === 0) { 20 | return true; 21 | } 22 | const request = context.switchToHttp().getRequest(); 23 | const user = request.user as AdminEntity; 24 | if (!requiredRoles.includes(user.role)) { 25 | throw new ForbiddenException('Permission deny'); 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/guards/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/guards" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paginationToQuery'; 2 | export * from './queryBuilder'; 3 | export * from './setCookieRFToken'; 4 | -------------------------------------------------------------------------------- /libs/helpers/src/paginationToQuery.ts: -------------------------------------------------------------------------------- 1 | import { PaginationDto } from '@app/base'; 2 | 3 | export const PaginationToQuery = (pagination: PaginationDto) => { 4 | const limit = pagination.limit ? pagination.limit : '10'; 5 | const page = pagination.page ? pagination.page : '1'; 6 | const skip = parseInt(limit) * (parseInt(page) - 1); 7 | const take = parseInt(limit); 8 | return { skip, take }; 9 | }; 10 | -------------------------------------------------------------------------------- /libs/helpers/src/queryBuilder.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from '@app/base'; 2 | import { FindOptionsOrder, FindOptionsWhere, Repository, SelectQueryBuilder } from 'typeorm'; 3 | import { PaginationToQuery } from './paginationToQuery'; 4 | 5 | export function getQueryBuilder( 6 | repo: Repository, 7 | query: any, 8 | where?: FindOptionsWhere | FindOptionsWhere[], 9 | order?: FindOptionsOrder, 10 | ...relations: string[] 11 | ) { 12 | const { skip, take } = PaginationToQuery(query); 13 | delete query.limit; 14 | delete query.page; 15 | let queryBuilder = repo.createQueryBuilder('entity'); 16 | queryBuilder = addRelations(queryBuilder, relations); 17 | if (where) { 18 | queryBuilder = addWhere(queryBuilder, where); 19 | queryBuilder = addQuery(queryBuilder, where, query); 20 | } 21 | queryBuilder.skip(skip).take(take); 22 | for (const orderKey in order) { 23 | queryBuilder.orderBy(`entity.${orderKey}`, order[orderKey] as any); 24 | } 25 | return queryBuilder; 26 | } 27 | 28 | function addRelations( 29 | queryBuilder: SelectQueryBuilder, 30 | relations: string[] 31 | ) { 32 | relations.forEach((relation) => { 33 | queryBuilder.leftJoinAndSelect(`entity.${relation}`, relation); 34 | }); 35 | return queryBuilder; 36 | } 37 | function addWhere( 38 | queryBuilder: SelectQueryBuilder, 39 | where: FindOptionsWhere | FindOptionsWhere[] 40 | ) { 41 | const whereKeys = Object.keys(where); 42 | for (let i = 0; i < whereKeys.length; i++) { 43 | const key = whereKeys[i]; 44 | let queryString = `entity.${key} = ${where[key]}`; 45 | if (typeof where[key] === 'string') { 46 | queryString = `entity.${key} = '${where[key]}'`; 47 | } 48 | let whereMethod = 'andWhere'; 49 | if (i === 0) { 50 | whereMethod = 'where'; 51 | } 52 | queryBuilder[whereMethod](queryString); 53 | } 54 | return queryBuilder; 55 | } 56 | function addQuery( 57 | queryBuilder: SelectQueryBuilder, 58 | where: FindOptionsWhere | FindOptionsWhere[], 59 | query: any 60 | ) { 61 | const whereKeys = Object.keys(where); 62 | const queryKeys = Object.keys(query); 63 | queryKeys.forEach((key) => { 64 | const value: string = query[key]; 65 | const queryObjects = key.split('.'); 66 | let queryString = `entity.${key} LIKE '%${value}%'`; 67 | let whereMethod = 'andWhere'; 68 | if (whereKeys.length === 0) { 69 | whereMethod = 'where'; 70 | } 71 | if (queryObjects.length > 1) { 72 | queryString = `${key} LIKE '%${value}%'`; 73 | } 74 | queryBuilder[whereMethod](queryString); 75 | }); 76 | return queryBuilder; 77 | } 78 | -------------------------------------------------------------------------------- /libs/helpers/src/setCookieRFToken.ts: -------------------------------------------------------------------------------- 1 | import { TokenExpires } from '@app/constants'; 2 | import { Response } from 'express'; 3 | 4 | export function SetCookieRFToken(response: Response, encryptId: string) { 5 | response.cookie('sub', encryptId, { 6 | maxAge: TokenExpires.redisRefreshToken, 7 | httpOnly: true 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /libs/helpers/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/helpers" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/interceptors/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './response-transform.interceptor'; 2 | -------------------------------------------------------------------------------- /libs/interceptors/src/response-transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { FormatResponse } from '@app/interfaces'; 2 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 3 | import { Request, Response } from 'express'; 4 | import { Observable, map } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class ResponseTransformInterceptor implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | const http = context.switchToHttp(); 10 | const response = http.getResponse(); 11 | const request = http.getRequest(); 12 | const statusCode = response.statusCode; 13 | 14 | return next.handle().pipe( 15 | map((data) => { 16 | if (isPagination(data)) { 17 | return { 18 | statusCode, 19 | success: true, 20 | data: data[0], 21 | metadata: getMetadata(request, data) 22 | }; 23 | } 24 | 25 | return { 26 | statusCode, 27 | success: true, 28 | data 29 | }; 30 | }) 31 | ); 32 | } 33 | } 34 | 35 | const isPagination = (data: any): boolean => { 36 | if (!Array.isArray(data)) return false; 37 | if (data.length !== 2) return false; 38 | const [entities, count] = data; 39 | if (!Array.isArray(entities)) return false; 40 | if (typeof count !== 'number') return false; 41 | 42 | return true; 43 | }; 44 | 45 | const getMetadata = (req: Request, data: any[]) => { 46 | const { page: pageQuery, limit: limitQuery } = req.query; 47 | const page = pageQuery ? +pageQuery : 1; 48 | const limit = limitQuery ? +limitQuery : 10; 49 | const totalItems = data[1]; 50 | const totalPages = Math.ceil(data[1] / +limit); 51 | 52 | return { 53 | page, 54 | limit, 55 | totalItems, 56 | totalPages 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /libs/interceptors/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/interceptors" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/interfaces/src/format-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { Pagination } from './pagination.interface'; 2 | 3 | export interface FormatResponse { 4 | statusCode: number; 5 | success: boolean; 6 | data: any; 7 | errors?: any; 8 | metadata?: Pagination; 9 | } 10 | -------------------------------------------------------------------------------- /libs/interfaces/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './format-response.interface'; 2 | export * from './pagination.interface'; 3 | export * from './redis.interface'; 4 | -------------------------------------------------------------------------------- /libs/interfaces/src/pagination.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Pagination { 2 | limit: number; 3 | page: number; 4 | totalPages: number; 5 | totalItems: number; 6 | } 7 | -------------------------------------------------------------------------------- /libs/interfaces/src/redis.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RedisType { 2 | key: string; 3 | value: string; 4 | expired: string | number; 5 | } 6 | -------------------------------------------------------------------------------- /libs/interfaces/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/interfaces" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/jwt/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.interface'; 2 | export * from './jwt.module'; 3 | export * from './jwt.service'; 4 | -------------------------------------------------------------------------------- /libs/jwt/src/jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayload { 2 | id: string; 3 | } 4 | -------------------------------------------------------------------------------- /libs/jwt/src/jwt.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { JwtService } from './jwt.service'; 4 | 5 | @Global() 6 | @Module({ 7 | imports: [JwtModule.register({})], 8 | providers: [JwtService], 9 | exports: [JwtService] 10 | }) 11 | export class JWTModule {} 12 | -------------------------------------------------------------------------------- /libs/jwt/src/jwt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { JwtService as NestJwtService } from '@nestjs/jwt'; 3 | import { JwtPayload } from './jwt.interface'; 4 | 5 | @Injectable() 6 | export class JwtService extends NestJwtService { 7 | signJwt(payload: JwtPayload, isRefreshToken = false) { 8 | const expiresIn = isRefreshToken ? '30d' : '15d'; 9 | const token = this.sign(payload, { 10 | expiresIn, 11 | secret: process.env.SECRET_JWT 12 | }); 13 | 14 | return token; 15 | } 16 | 17 | async verifyJwt(token: string) { 18 | try { 19 | const payload = await this.verify(token, { 20 | secret: process.env.SECRET_JWT 21 | }); 22 | 23 | return payload; 24 | } catch (error) { 25 | throw new UnauthorizedException(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/jwt/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/jwt" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/redis/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redis.module'; 2 | export * from './redis.service'; 3 | -------------------------------------------------------------------------------- /libs/redis/src/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { MetadataKey } from '@app/constants'; 2 | import { Global, Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import Redis from 'ioredis'; 5 | import { RedisService } from './redis.service'; 6 | 7 | @Global() 8 | @Module({ 9 | providers: [ 10 | { 11 | provide: MetadataKey.REDIS, 12 | useFactory(config: ConfigService) { 13 | return new Redis({ 14 | port: config.get('REDIS_PORT'), 15 | host: config.get('REDIS_HOST'), 16 | db: config.get('REDIS_DB'), 17 | password: config.get('REDIS_PASSWORD') 18 | }); 19 | }, 20 | inject: [ConfigService] 21 | }, 22 | RedisService 23 | ], 24 | exports: [RedisService] 25 | }) 26 | export class RedisModule {} 27 | -------------------------------------------------------------------------------- /libs/redis/src/redis.service.ts: -------------------------------------------------------------------------------- 1 | import { MetadataKey, TokenExpires } from '@app/constants'; 2 | import { RedisType } from '@app/interfaces/redis.interface'; 3 | import { Inject, Injectable, NotFoundException } from '@nestjs/common'; 4 | import Redis from 'ioredis'; 5 | 6 | @Injectable() 7 | export class RedisService { 8 | constructor(@Inject(MetadataKey.REDIS) private redis: Redis) {} 9 | 10 | set(redisData: RedisType): Promise<'OK'> { 11 | const { key, value, expired } = redisData; 12 | return this.redis.set(key, value, 'EX', expired); 13 | } 14 | 15 | setNx(redisData: RedisType): Promise { 16 | return this.redis.setnx(redisData.key, redisData.value); 17 | } 18 | 19 | get(key: string): Promise { 20 | return this.redis.get(key); 21 | } 22 | async getRefreshToken(sub: string) { 23 | const key = `RF_TOKEN:${sub}`; 24 | const getRfToken = await this.get(key); 25 | if (!getRfToken) { 26 | throw new NotFoundException('Refresh token not found'); 27 | } 28 | return getRfToken; 29 | } 30 | async getAccessToken(sub: string) { 31 | const key = `AC_TOKEN:${sub}`; 32 | const accessToken = await this.get(key); 33 | if (!accessToken) { 34 | throw new NotFoundException('Access token not found'); 35 | } 36 | return accessToken; 37 | } 38 | async setRefreshToken(sub: string, token: string) { 39 | const key = `RF_TOKEN:${sub}`; 40 | return this.set({ 41 | key, 42 | value: token, 43 | expired: TokenExpires.redisRefreshToken 44 | }); 45 | } 46 | async setAccessToken(sub: string, token: string) { 47 | const key = `AC_TOKEN:${sub}`; 48 | return this.set({ 49 | key, 50 | value: token, 51 | expired: TokenExpires.redisAccessToken 52 | }); 53 | } 54 | async del(key: string) { 55 | return this.redis.del(key); 56 | } 57 | 58 | async delRFToken(sub: string) { 59 | const key = `RF_TOKEN:${sub}`; 60 | return this.redis.del(key); 61 | } 62 | 63 | async delAccessToken(sub: string) { 64 | const key = `AC_TOKEN:${sub}`; 65 | return this.redis.del(key); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /libs/redis/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/redis" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "plugins": [ 8 | "@nestjs/swagger" 9 | ], 10 | "webpack": true 11 | }, 12 | "projects": { 13 | "jwt": { 14 | "type": "library", 15 | "root": "libs/jwt", 16 | "entryFile": "index", 17 | "sourceRoot": "libs/jwt/src", 18 | "compilerOptions": { 19 | "tsConfigPath": "libs/jwt/tsconfig.lib.json" 20 | } 21 | }, 22 | "crypto": { 23 | "type": "library", 24 | "root": "libs/crypto", 25 | "entryFile": "index", 26 | "sourceRoot": "libs/crypto/src", 27 | "compilerOptions": { 28 | "tsConfigPath": "libs/crypto/tsconfig.lib.json" 29 | } 30 | }, 31 | "redis": { 32 | "type": "library", 33 | "root": "libs/redis", 34 | "entryFile": "index", 35 | "sourceRoot": "libs/redis/src", 36 | "compilerOptions": { 37 | "tsConfigPath": "libs/redis/tsconfig.lib.json" 38 | } 39 | }, 40 | "database": { 41 | "type": "library", 42 | "root": "libs/database", 43 | "entryFile": "index", 44 | "sourceRoot": "libs/database/src", 45 | "compilerOptions": { 46 | "tsConfigPath": "libs/database/tsconfig.lib.json" 47 | } 48 | }, 49 | "base": { 50 | "type": "library", 51 | "root": "libs/base", 52 | "entryFile": "index", 53 | "sourceRoot": "libs/base/src", 54 | "compilerOptions": { 55 | "tsConfigPath": "libs/base/tsconfig.lib.json" 56 | } 57 | }, 58 | "decorators": { 59 | "type": "library", 60 | "root": "libs/decorators", 61 | "entryFile": "index", 62 | "sourceRoot": "libs/decorators/src", 63 | "compilerOptions": { 64 | "tsConfigPath": "libs/decorators/tsconfig.lib.json" 65 | } 66 | }, 67 | "filters": { 68 | "type": "library", 69 | "root": "libs/filters", 70 | "entryFile": "index", 71 | "sourceRoot": "libs/filters/src", 72 | "compilerOptions": { 73 | "tsConfigPath": "libs/filters/tsconfig.lib.json" 74 | } 75 | }, 76 | "guards": { 77 | "type": "library", 78 | "root": "libs/guards", 79 | "entryFile": "index", 80 | "sourceRoot": "libs/guards/src", 81 | "compilerOptions": { 82 | "tsConfigPath": "libs/guards/tsconfig.lib.json" 83 | } 84 | }, 85 | "interceptors": { 86 | "type": "library", 87 | "root": "libs/interceptors", 88 | "entryFile": "index", 89 | "sourceRoot": "libs/interceptors/src", 90 | "compilerOptions": { 91 | "tsConfigPath": "libs/interceptors/tsconfig.lib.json" 92 | } 93 | }, 94 | "helpers": { 95 | "type": "library", 96 | "root": "libs/helpers", 97 | "entryFile": "index", 98 | "sourceRoot": "libs/helpers/src", 99 | "compilerOptions": { 100 | "tsConfigPath": "libs/helpers/tsconfig.lib.json" 101 | } 102 | }, 103 | "enums": { 104 | "type": "library", 105 | "root": "libs/enums", 106 | "entryFile": "index", 107 | "sourceRoot": "libs/enums/src", 108 | "compilerOptions": { 109 | "tsConfigPath": "libs/enums/tsconfig.lib.json" 110 | } 111 | }, 112 | "interfaces": { 113 | "type": "library", 114 | "root": "libs/interfaces", 115 | "entryFile": "index", 116 | "sourceRoot": "libs/interfaces/src", 117 | "compilerOptions": { 118 | "tsConfigPath": "libs/interfaces/tsconfig.lib.json" 119 | } 120 | }, 121 | "constants": { 122 | "type": "library", 123 | "root": "libs/constants", 124 | "entryFile": "index", 125 | "sourceRoot": "libs/constants/src", 126 | "compilerOptions": { 127 | "tsConfigPath": "libs/constants/tsconfig.lib.json" 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /ormconfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require('dotenv').config({ 3 | path: `.env.${process.env.NODE_ENV}` 4 | }); 5 | 6 | import { DataSource, DataSourceOptions } from 'typeorm'; 7 | 8 | const { DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME, NODE_ENV } = process.env; 9 | 10 | export const options: DataSourceOptions = { 11 | type: 'postgres', 12 | host: DB_HOST, 13 | port: DB_PORT ? +DB_PORT : 5432, 14 | username: DB_USERNAME, 15 | password: DB_PASSWORD, 16 | database: DB_NAME, 17 | migrationsTableName: 'migrations', 18 | migrations: [], 19 | synchronize: NODE_ENV !== 'production' 20 | }; 21 | 22 | export const AppDataSource = new DataSource(options); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-base", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", 11 | "start": "NODE_ENV=production nest start", 12 | "start:dev": "NODE_ENV=development nest start --watch", 13 | "start:debug": "NODE_ENV=development nest start --debug --watch", 14 | "start:prod": "NODE_ENV=production node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "NODE_ENV=test jest", 17 | "test:watch": "NODE_ENV=test jest --watch", 18 | "test:cov": "NODE_ENV=test jest --coverage", 19 | "test:debug": "NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json", 21 | "prepare": "patch-package && husky install" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/config": "^2.3.3", 26 | "@nestjs/core": "^9.0.0", 27 | "@nestjs/devtools-integration": "^0.1.4", 28 | "@nestjs/jwt": "^10.0.3", 29 | "@nestjs/passport": "^9.0.3", 30 | "@nestjs/platform-express": "^9.0.0", 31 | "@nestjs/platform-socket.io": "^9.4.3", 32 | "@nestjs/serve-static": "^3.0.1", 33 | "@nestjs/swagger": "^6.3.0", 34 | "@nestjs/typeorm": "^9.0.1", 35 | "@nestjs/websockets": "^9.4.3", 36 | "@types/multer": "^1.4.7", 37 | "@types/passport-jwt": "^3.0.8", 38 | "@types/passport-local": "^1.0.35", 39 | "add": "^2.0.6", 40 | "argon2": "^0.30.3", 41 | "class-transformer": "^0.5.1", 42 | "class-validator": "^0.14.0", 43 | "cookie-parser": "^1.4.6", 44 | "husky": "^8.0.3", 45 | "ioredis": "^5.3.2", 46 | "passport": "^0.6.0", 47 | "passport-jwt": "^4.0.1", 48 | "passport-local": "^1.0.0", 49 | "patch-package": "^7.0.0", 50 | "pg": "^8.11.0", 51 | "postinstall-postinstall": "^2.1.0", 52 | "reflect-metadata": "^0.1.13", 53 | "rxjs": "^7.2.0", 54 | "socket.io": "^4.6.2", 55 | "ts-loader": "^9.4.3", 56 | "tsc-alias": "^1.8.6", 57 | "typeorm": "^0.3.16", 58 | "yarn": "^1.22.19" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "^17.6.5", 62 | "@commitlint/config-conventional": "^17.6.5", 63 | "@darraghor/eslint-plugin-nestjs-typed": "^3.22.6", 64 | "@nestjs/cli": "^9.0.0", 65 | "@nestjs/schematics": "^9.0.0", 66 | "@nestjs/testing": "^9.0.0", 67 | "@types/cookie-parser": "^1.4.3", 68 | "@types/express": "^4.17.13", 69 | "@types/jest": "29.5.0", 70 | "@types/node": "18.15.11", 71 | "@types/supertest": "^2.0.11", 72 | "@typescript-eslint/eslint-plugin": "^5.0.0", 73 | "@typescript-eslint/parser": "^5.0.0", 74 | "eslint": "^8.0.1", 75 | "eslint-config-prettier": "^8.3.0", 76 | "eslint-plugin-prettier": "^4.0.0", 77 | "jest": "29.5.0", 78 | "lint-staged": "^13.2.2", 79 | "prettier": "^2.3.2", 80 | "source-map-support": "^0.5.20", 81 | "supertest": "^6.1.3", 82 | "ts-jest": "29.0.5", 83 | "ts-node": "^10.0.0", 84 | "tsconfig-paths": "4.2.0", 85 | "typescript": "^4.7.4" 86 | }, 87 | "jest": { 88 | "moduleFileExtensions": [ 89 | "js", 90 | "json", 91 | "ts" 92 | ], 93 | "rootDir": ".", 94 | "testRegex": ".*\\.spec\\.ts$", 95 | "transform": { 96 | "^.+\\.(t|j)s$": "ts-jest" 97 | }, 98 | "collectCoverageFrom": [ 99 | "**/*.(t|j)s" 100 | ], 101 | "coverageDirectory": "./coverage", 102 | "testEnvironment": "node", 103 | "roots": [ 104 | "/src/", 105 | "/libs/" 106 | ], 107 | "moduleNameMapper": { 108 | "^@app/jwt(|/.*)$": "/libs/jwt/src/$1", 109 | "^@app/crypto(|/.*)$": "/libs/crypto/src/$1", 110 | "^@app/redis(|/.*)$": "/libs/redis/src/$1", 111 | "^@app/database(|/.*)$": "/libs/database/src/$1", 112 | "^@app/base(|/.*)$": "/libs/base/src/$1", 113 | "^@app/decorators(|/.*)$": "/libs/decorators/src/$1", 114 | "^@app/filters(|/.*)$": "/libs/filters/src/$1", 115 | "^@app/guards(|/.*)$": "/libs/guards/src/$1", 116 | "^@app/interceptors(|/.*)$": "/libs/interceptors/src/$1", 117 | "^@app/helpers(|/.*)$": "/libs/helpers/src/$1", 118 | "^@app/enums(|/.*)$": "/libs/enums/src/$1", 119 | "^@app/interfaces(|/.*)$": "/libs/interfaces/src/$1", 120 | "^@app/constants(|/.*)$": "/libs/constants/src/$1" 121 | } 122 | }, 123 | "lint-staged": { 124 | "*.ts": [ 125 | "npm run lint", 126 | "npm run format" 127 | ] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /patches/@nestjs+schematics+9.2.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.ts 2 | deleted file mode 100644 3 | index 8d1b7b4..0000000 4 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.ts 5 | +++ /dev/null 6 | @@ -1,63 +0,0 @@ 7 | -<% if (crud && type === 'rest') { %>import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';<% 8 | -} else if (crud && type === 'microservice') { %>import { Controller } from '@nestjs/common'; 9 | -import { MessagePattern, Payload } from '@nestjs/microservices';<% 10 | -} else { %>import { Controller } from '@nestjs/common';<% 11 | -} %> 12 | -import { <%= classify(name) %>Service } from './<%= name %>.service';<% if (crud) { %> 13 | -import { Create<%= singular(classify(name)) %>Dto } from './dto/create-<%= singular(name) %>.dto'; 14 | -import { Update<%= singular(classify(name)) %>Dto } from './dto/update-<%= singular(name) %>.dto';<% } %> 15 | - 16 | -<% if (type === 'rest') { %>@Controller('<%= dasherize(name) %>')<% } else { %>@Controller()<% } %> 17 | -export class <%= classify(name) %>Controller { 18 | - constructor(private readonly <%= lowercased(name) %>Service: <%= classify(name) %>Service) {}<% if (type === 'rest' && crud) { %> 19 | - 20 | - @Post() 21 | - create(@Body() create<%= singular(classify(name)) %>Dto: Create<%= singular(classify(name)) %>Dto) { 22 | - return this.<%= lowercased(name) %>Service.create(create<%= singular(classify(name)) %>Dto); 23 | - } 24 | - 25 | - @Get() 26 | - findAll() { 27 | - return this.<%= lowercased(name) %>Service.findAll(); 28 | - } 29 | - 30 | - @Get(':id') 31 | - findOne(@Param('id') id: string) { 32 | - return this.<%= lowercased(name) %>Service.findOne(+id); 33 | - } 34 | - 35 | - @Patch(':id') 36 | - update(@Param('id') id: string, @Body() update<%= singular(classify(name)) %>Dto: Update<%= singular(classify(name)) %>Dto) { 37 | - return this.<%= lowercased(name) %>Service.update(+id, update<%= singular(classify(name)) %>Dto); 38 | - } 39 | - 40 | - @Delete(':id') 41 | - remove(@Param('id') id: string) { 42 | - return this.<%= lowercased(name) %>Service.remove(+id); 43 | - }<% } else if (type === 'microservice' && crud) { %> 44 | - 45 | - @MessagePattern('create<%= singular(classify(name)) %>') 46 | - create(@Payload() create<%= singular(classify(name)) %>Dto: Create<%= singular(classify(name)) %>Dto) { 47 | - return this.<%= lowercased(name) %>Service.create(create<%= singular(classify(name)) %>Dto); 48 | - } 49 | - 50 | - @MessagePattern('findAll<%= classify(name) %>') 51 | - findAll() { 52 | - return this.<%= lowercased(name) %>Service.findAll(); 53 | - } 54 | - 55 | - @MessagePattern('findOne<%= singular(classify(name)) %>') 56 | - findOne(@Payload() id: number) { 57 | - return this.<%= lowercased(name) %>Service.findOne(id); 58 | - } 59 | - 60 | - @MessagePattern('update<%= singular(classify(name)) %>') 61 | - update(@Payload() update<%= singular(classify(name)) %>Dto: Update<%= singular(classify(name)) %>Dto) { 62 | - return this.<%= lowercased(name) %>Service.update(update<%= singular(classify(name)) %>Dto.id, update<%= singular(classify(name)) %>Dto); 63 | - } 64 | - 65 | - @MessagePattern('remove<%= singular(classify(name)) %>') 66 | - remove(@Payload() id: number) { 67 | - return this.<%= lowercased(name) %>Service.remove(id); 68 | - }<% } %> 69 | -} 70 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.module.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.module.ts 71 | index c45e860..6e0748a 100644 72 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.module.ts 73 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.module.ts 74 | @@ -1,9 +1,13 @@ 75 | import { Module } from '@nestjs/common'; 76 | -import { <%= classify(name) %>Service } from './<%= name %>.service'; 77 | -<% if (type === 'rest' || type === 'microservice') { %>import { <%= classify(name) %>Controller } from './<%= name %>.controller';<% } %><% if (type === 'graphql-code-first' || type === 'graphql-schema-first') { %>import { <%= classify(name) %>Resolver } from './<%= name %>.resolver';<% } %><% if (type === 'ws') { %>import { <%= classify(name) %>Gateway } from './<%= name %>.gateway';<% } %> 78 | +import { <%= classify(name) %>Service } from './services/<%= name %>.service'; 79 | +<% if (type === 'rest' || type === 'microservice') { %>import { <%= classify(name) %>Controller } from './controllers/<%= name %>.controller';<% } %><% if (type === 'graphql-code-first' || type === 'graphql-schema-first') { %>import { <%= classify(name) %>Resolver } from './resolvers/<%= name %>.resolver';<% } %><% if (type === 'ws') { %>import { <%= classify(name) %>Gateway } from './gateway/<%= name %>.gateway';<% } %> 80 | +<% if (crud) { %>import { TypeOrmModule } from '@nestjs/typeorm'; 81 | +import { <%= classify(name) %>Entity } from './entities/<%= name %>.entity';<% }%> 82 | 83 | @Module({ 84 | - <% if (type === 'rest' || type === 'microservice') { %>controllers: [<%= classify(name) %>Controller], 85 | - providers: [<%= classify(name) %>Service]<% } else if (type === 'graphql-code-first' || type === 'graphql-schema-first') { %>providers: [<%= classify(name) %>Resolver, <%= classify(name) %>Service]<% } else { %>providers: [<%= classify(name) %>Gateway, <%= classify(name) %>Service]<% } %> 86 | + <% if (type === 'rest' || type === 'microservice') { %><% if (crud) {%>imports: [TypeOrmModule.forFeature([<%= classify(name) %>Entity])], 87 | + <%}%>controllers: [<%= classify(name) %>Controller], 88 | + providers: [<%= classify(name) %>Service], 89 | + exports: [<%= classify(name) %>Service]<% } else if (type === 'graphql-code-first' || type === 'graphql-schema-first') { %>providers: [<%= classify(name) %>Resolver, <%= classify(name) %>Service]<% } else { %>providers: [<%= classify(name) %>Gateway, <%= classify(name) %>Service]<% } %> 90 | }) 91 | export class <%= classify(name) %>Module {} 92 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.ts 93 | deleted file mode 100644 94 | index 21943aa..0000000 95 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.ts 96 | +++ /dev/null 97 | @@ -1,28 +0,0 @@ 98 | -import { Injectable } from '@nestjs/common';<% if (crud && type !== 'graphql-code-first' && type !== 'graphql-schema-first') { %> 99 | -import { Create<%= singular(classify(name)) %>Dto } from './dto/create-<%= singular(name) %>.dto'; 100 | -import { Update<%= singular(classify(name)) %>Dto } from './dto/update-<%= singular(name) %>.dto';<% } else if (crud) { %> 101 | -import { Create<%= singular(classify(name)) %>Input } from './dto/create-<%= singular(name) %>.input'; 102 | -import { Update<%= singular(classify(name)) %>Input } from './dto/update-<%= singular(name) %>.input';<% } %> 103 | - 104 | -@Injectable() 105 | -export class <%= classify(name) %>Service {<% if (crud) { %> 106 | - create(<% if (type !== 'graphql-code-first' && type !== 'graphql-schema-first') { %>create<%= singular(classify(name)) %>Dto: Create<%= singular(classify(name)) %>Dto<% } else { %>create<%= singular(classify(name)) %>Input: Create<%= singular(classify(name)) %>Input<% } %>) { 107 | - return 'This action adds a new <%= lowercased(singular(classify(name))) %>'; 108 | - } 109 | - 110 | - findAll() { 111 | - return `This action returns all <%= lowercased(classify(name)) %>`; 112 | - } 113 | - 114 | - findOne(id: number) { 115 | - return `This action returns a #${id} <%= lowercased(singular(classify(name))) %>`; 116 | - } 117 | - 118 | - update(id: number, <% if (type !== 'graphql-code-first' && type !== 'graphql-schema-first') { %>update<%= singular(classify(name)) %>Dto: Update<%= singular(classify(name)) %>Dto<% } else { %>update<%= singular(classify(name)) %>Input: Update<%= singular(classify(name)) %>Input<% } %>) { 119 | - return `This action updates a #${id} <%= lowercased(singular(classify(name))) %>`; 120 | - } 121 | - 122 | - remove(id: number) { 123 | - return `This action removes a #${id} <%= lowercased(singular(classify(name))) %>`; 124 | - } 125 | -<% } %>} 126 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/controllers/__name__.controller.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/controllers/__name__.controller.ts 127 | new file mode 100644 128 | index 0000000..ac8332a 129 | --- /dev/null 130 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/controllers/__name__.controller.ts 131 | @@ -0,0 +1,58 @@ 132 | +<% if (crud && type === 'rest') { %>import { Controller, Post, Body, Patch, Param } from '@nestjs/common';<% 133 | +} else if (crud && type === 'microservice') { %>import { Controller } from '@nestjs/common'; 134 | +import { MessagePattern, Payload } from '@nestjs/microservices';<% 135 | +} else { %>import { Controller } from '@nestjs/common';<% 136 | +} %> 137 | +import { <%= classify(name) %>Service } from '../services/<%= name %>.service';<% if (crud) { %> 138 | +import { Create<%= singular(classify(name)) %>Dto } from '../dto/create-<%= singular(name) %>.dto'; 139 | +import { Update<%= singular(classify(name)) %>Dto } from '../dto/update-<%= singular(name) %>.dto'; 140 | +import { BaseController, ApiCreate, ApiUpdate } from '@app/base'; 141 | +import { <%= classify(name) %>Entity } from '../entities/<%= name %>.entity';<% } %> 142 | +import { ApiTags } from '@nestjs/swagger'; 143 | + 144 | +<% if (type === 'rest') { %>@Controller('<%= dasherize(name) %>')<% } else { %>@Controller()<% } %> 145 | +@ApiTags('<%= classify(name) %> API') 146 | +export class <%= classify(name) %>Controller<% if (crud && type === 'rest') { %> extends BaseController<<%= classify(name) %>Entity>(<%= classify(name) %>Entity, '<%= lowercased(name) %>')<% } %> { 147 | + <% if (type === 'rest' && crud) { %>relations = [];<% } %> 148 | + 149 | + constructor(private readonly <%= lowercased(name) %>Service: <%= classify(name) %>Service) {<% if (type === 'rest' && crud) { %> 150 | + super(<%= lowercased(name) %>Service); 151 | + <%}%>}<% if (type === 'rest' && crud) { %> 152 | + 153 | + @Post() 154 | + @ApiCreate(<%= classify(name) %>Entity, '<%= lowercased(name) %>') 155 | + create(@Body() body: Create<%= singular(classify(name)) %>Dto) { 156 | + return super.create(body); 157 | + } 158 | + 159 | + @Patch(':id') 160 | + @ApiUpdate(<%= classify(name) %>Entity, '<%= lowercased(name) %>') 161 | + update(@Param('id') id: string, @Body() body: Update<%= singular(classify(name)) %>Dto) { 162 | + return super.update(id, body); 163 | + }<% } else if (type === 'microservice' && crud) { %> 164 | + 165 | + @MessagePattern('create<%= singular(classify(name)) %>') 166 | + create(@Payload() create<%= singular(classify(name)) %>Dto: Create<%= singular(classify(name)) %>Dto) { 167 | + return this.<%= lowercased(name) %>Service.create(create<%= singular(classify(name)) %>Dto); 168 | + } 169 | + 170 | + @MessagePattern('findAll<%= classify(name) %>') 171 | + findAll() { 172 | + return this.<%= lowercased(name) %>Service.findAll(); 173 | + } 174 | + 175 | + @MessagePattern('findOne<%= singular(classify(name)) %>') 176 | + findOne(@Payload() id: number) { 177 | + return this.<%= lowercased(name) %>Service.findOne(id); 178 | + } 179 | + 180 | + @MessagePattern('update<%= singular(classify(name)) %>') 181 | + update(@Payload() update<%= singular(classify(name)) %>Dto: Update<%= singular(classify(name)) %>Dto) { 182 | + return this.<%= lowercased(name) %>Service.update(update<%= singular(classify(name)) %>Dto.id, update<%= singular(classify(name)) %>Dto); 183 | + } 184 | + 185 | + @MessagePattern('remove<%= singular(classify(name)) %>') 186 | + remove(@Payload() id: number) { 187 | + return this.<%= lowercased(name) %>Service.remove(id); 188 | + }<% } %> 189 | +} 190 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/entities/__name@singular@ent__.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/entities/__name@singular@ent__.ts 191 | index 362e741..a621ea3 100644 192 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/entities/__name@singular@ent__.ts 193 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/entities/__name@singular@ent__.ts 194 | @@ -4,4 +4,8 @@ 195 | export class <%= singular(classify(name)) %> { 196 | @Field(() => Int, { description: 'Example field (placeholder)' }) 197 | exampleField: number; 198 | -}<% } else { %>export class <%= singular(classify(name)) %> {}<% } %> 199 | +}<% } else { %>import { Entity } from 'typeorm'; 200 | +import { BaseEntity } from '@app/base'; 201 | + 202 | +@Entity({ name: '<%= underscore(name) %>' }) 203 | +export class <%= singular(classify(name)) %>Entity extends BaseEntity {}<% } %> 204 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.gateway.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/gateway/__name__.gateway.ts 205 | similarity index 100% 206 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.gateway.ts 207 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/gateway/__name__.gateway.ts 208 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.graphql b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/graphql/__name__.graphql 209 | similarity index 100% 210 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.graphql 211 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/graphql/__name__.graphql 212 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.resolver.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/resolvers/__name__.resolver.ts 213 | similarity index 100% 214 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.resolver.ts 215 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/resolvers/__name__.resolver.ts 216 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/services/__name__.service.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/services/__name__.service.ts 217 | new file mode 100644 218 | index 0000000..1c104c9 219 | --- /dev/null 220 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/services/__name__.service.ts 221 | @@ -0,0 +1,19 @@ 222 | +import { Injectable } from '@nestjs/common';<% if (crud && type !== 'graphql-code-first' && type !== 'graphql-schema-first') { %> 223 | +import { InjectRepository } from '@nestjs/typeorm'; 224 | +import { BaseService } from '@app/base'; 225 | +import { Repository } from 'typeorm'; 226 | +import { <%= classify(name) %>Entity } from '../entities/<%= name %>.entity';<% } else if (crud) { %> 227 | +import { Create<%= singular(classify(name)) %>Input } from './dto/create-<%= singular(name) %>.input'; 228 | +import { Update<%= singular(classify(name)) %>Input } from './dto/update-<%= singular(name) %>.input';<% } %> 229 | + 230 | +@Injectable() 231 | +export class <%= classify(name) %>Service<% if (crud) { %> extends BaseService<<%= classify(name) %>Entity><% } %> { 232 | + <% if (crud) { %>name = '<%= classify(name) %>'; 233 | + 234 | + constructor( 235 | + @InjectRepository(<%= classify(name) %>Entity) 236 | + private readonly <%= lowercased(name) %>Repo: Repository<<%= classify(name) %>Entity> 237 | + ) { 238 | + super(<%= lowercased(name) %>Repo); 239 | + }<% } %> 240 | +} 241 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.__specFileSuffix__.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.controller.__specFileSuffix__.ts 242 | similarity index 76% 243 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.__specFileSuffix__.ts 244 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.controller.__specFileSuffix__.ts 245 | index 17e5843..cfef2d1 100644 246 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.controller.__specFileSuffix__.ts 247 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.controller.__specFileSuffix__.ts 248 | @@ -1,6 +1,6 @@ 249 | import { Test, TestingModule } from '@nestjs/testing'; 250 | -import { <%= classify(name) %>Controller } from './<%= name %>.controller'; 251 | -import { <%= classify(name) %>Service } from './<%= name %>.service'; 252 | +import { <%= classify(name) %>Controller } from '../controllers/<%= name %>.controller'; 253 | +import { <%= classify(name) %>Service } from '../services/<%= name %>.service'; 254 | 255 | describe('<%= classify(name) %>Controller', () => { 256 | let controller: <%= classify(name) %>Controller; 257 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.gateway.__specFileSuffix__.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.gateway.__specFileSuffix__.ts 258 | similarity index 76% 259 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.gateway.__specFileSuffix__.ts 260 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.gateway.__specFileSuffix__.ts 261 | index 8f8b5be..ae27c44 100644 262 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.gateway.__specFileSuffix__.ts 263 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.gateway.__specFileSuffix__.ts 264 | @@ -1,6 +1,6 @@ 265 | import { Test, TestingModule } from '@nestjs/testing'; 266 | -import { <%= classify(name) %>Gateway } from './<%= name %>.gateway'; 267 | -import { <%= classify(name) %>Service } from './<%= name %>.service'; 268 | +import { <%= classify(name) %>Gateway } from '../gateway/<%= name %>.gateway'; 269 | +import { <%= classify(name) %>Service } from '../services/<%= name %>.service'; 270 | 271 | describe('<%= classify(name) %>Gateway', () => { 272 | let gateway: <%= classify(name) %>Gateway; 273 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.resolver.__specFileSuffix__.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.resolver.__specFileSuffix__.ts 274 | similarity index 76% 275 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.resolver.__specFileSuffix__.ts 276 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.resolver.__specFileSuffix__.ts 277 | index 2ef2c6f..8918958 100644 278 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.resolver.__specFileSuffix__.ts 279 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.resolver.__specFileSuffix__.ts 280 | @@ -1,6 +1,6 @@ 281 | import { Test, TestingModule } from '@nestjs/testing'; 282 | -import { <%= classify(name) %>Resolver } from './<%= name %>.resolver'; 283 | -import { <%= classify(name) %>Service } from './<%= name %>.service'; 284 | +import { <%= classify(name) %>Resolver } from '../resolvers/<%= name %>.resolver'; 285 | +import { <%= classify(name) %>Service } from '../services/<%= name %>.service'; 286 | 287 | describe('<%= classify(name) %>Resolver', () => { 288 | let resolver: <%= classify(name) %>Resolver; 289 | diff --git a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.__specFileSuffix__.ts b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.service.__specFileSuffix__.ts 290 | similarity index 85% 291 | rename from node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.__specFileSuffix__.ts 292 | rename to node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.service.__specFileSuffix__.ts 293 | index 2b3f38b..cfcc49f 100644 294 | --- a/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/__name__.service.__specFileSuffix__.ts 295 | +++ b/node_modules/@nestjs/schematics/dist/lib/resource/files/ts/test/__name__.service.__specFileSuffix__.ts 296 | @@ -1,5 +1,5 @@ 297 | import { Test, TestingModule } from '@nestjs/testing'; 298 | -import { <%= classify(name) %>Service } from './<%= name %>.service'; 299 | +import { <%= classify(name) %>Service } from '../services/<%= name %>.service'; 300 | 301 | describe('<%= classify(name) %>Service', () => { 302 | let service: <%= classify(name) %>Service; 303 | -------------------------------------------------------------------------------- /src/apis/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AdminController } from './controllers/admin.controller'; 4 | import { AdminEntity } from './entities/admin.entity'; 5 | import { AdminService } from './services/admin.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([AdminEntity])], 9 | controllers: [AdminController], 10 | providers: [AdminService], 11 | exports: [AdminService] 12 | }) 13 | export class AdminModule {} 14 | -------------------------------------------------------------------------------- /src/apis/admin/controllers/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiCreate, ApiGetDetail, ApiUpdate, BaseController } from '@app/base'; 2 | import { AuthAdmin } from '@app/decorators/auth-admin.decorator'; 3 | import { User } from '@app/decorators/user.decorator'; 4 | import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; 5 | import { ApiTags } from '@nestjs/swagger'; 6 | import { CreateAdminDto } from '../dto/create-admin.dto'; 7 | import { UpdateAdminDto } from '../dto/update-admin.dto'; 8 | import { AdminService } from '../services/admin.service'; 9 | import { AdminEntity } from './../entities/admin.entity'; 10 | 11 | @Controller('admin') 12 | @ApiTags('Admin API') 13 | @AuthAdmin() 14 | export class AdminController extends BaseController(AdminEntity, 'admin') { 15 | relations = []; 16 | 17 | constructor(private readonly adminService: AdminService) { 18 | super(adminService); 19 | } 20 | 21 | @Post('create') 22 | @ApiCreate(AdminEntity, 'admin') 23 | create(@Body() body: CreateAdminDto): Promise { 24 | return super.create(body); 25 | } 26 | @Patch('update/:id') 27 | @ApiUpdate(AdminEntity, 'admin') 28 | update(@Param('id') id: string, @Body() body: UpdateAdminDto): Promise { 29 | return super.update(id, body); 30 | } 31 | 32 | @Get('me') 33 | @ApiGetDetail(AdminEntity, 'admin') 34 | getMe(@User() user: AdminEntity) { 35 | return user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/apis/admin/dto/create-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { Roles } from '@app/enums/role.enum'; 2 | import { IsEnum, IsNotEmpty, IsNumberString, IsString } from 'class-validator'; 3 | 4 | export class CreateAdminDto { 5 | @IsNumberString() 6 | phone!: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | password!: string; 11 | 12 | @IsEnum(Roles) 13 | role!: Roles; 14 | } 15 | -------------------------------------------------------------------------------- /src/apis/admin/dto/update-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { OmitType, PartialType } from '@nestjs/swagger'; 2 | import { CreateAdminDto } from './create-admin.dto'; 3 | 4 | export class UpdateAdminDto extends OmitType(PartialType(CreateAdminDto), ['password']) {} 5 | -------------------------------------------------------------------------------- /src/apis/admin/entities/admin.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from '@app/base'; 2 | import { Roles } from '@app/enums/role.enum'; 3 | import { ApiHideProperty } from '@nestjs/swagger'; 4 | import * as argon2 from 'argon2'; 5 | import { Exclude } from 'class-transformer'; 6 | import { BeforeInsert, Column, Entity, Unique } from 'typeorm'; 7 | 8 | @Entity({ name: 'admin' }) 9 | @Unique('admin', ['phone']) 10 | export class AdminEntity extends BaseEntity { 11 | @Column() 12 | phone!: string; 13 | 14 | @Column() 15 | @Exclude() 16 | @ApiHideProperty() 17 | password!: string; 18 | 19 | @Column() 20 | role!: Roles; 21 | 22 | @BeforeInsert() 23 | async beforeInsert() { 24 | this.password = await argon2.hash(this.password); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/apis/admin/services/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from '@app/base'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { AdminEntity } from '../entities/admin.entity'; 6 | 7 | @Injectable() 8 | export class AdminService extends BaseService { 9 | name = 'Admin'; 10 | 11 | constructor( 12 | @InjectRepository(AdminEntity) 13 | private readonly adminRepo: Repository 14 | ) { 15 | super(adminRepo); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/apis/admin/test/admin.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdminController } from '../controllers/admin.controller'; 3 | import { AdminService } from '../services/admin.service'; 4 | 5 | describe('AdminController', () => { 6 | let controller: AdminController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [AdminController], 11 | providers: [AdminService] 12 | }).compile(); 13 | 14 | controller = module.get(AdminController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/apis/admin/test/admin.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdminService } from '../services/admin.service'; 3 | 4 | describe('AdminService', () => { 5 | let service: AdminService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AdminService] 10 | }).compile(); 11 | 12 | service = module.get(AdminService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/apis/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdminModule } from './admin/admin.module'; 3 | import { AuthModule } from './auth/auth.module'; 4 | import { ChatModule } from './chat/chat.module'; 5 | import { UploadModule } from './upload/upload.module'; 6 | 7 | @Module({ 8 | imports: [AdminModule, AuthModule, UploadModule, ChatModule] 9 | }) 10 | export class ApiModule {} 11 | -------------------------------------------------------------------------------- /src/apis/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { AdminModule } from '../admin/admin.module'; 4 | import { AuthAdminController } from './controllers/auth.admin.controller'; 5 | import { AuthService } from './services/auth.service'; 6 | import { JwtAdminStrategy } from './strategies/jwt/admin.jwt.strategy'; 7 | import { AdminStrategy } from './strategies/local/admin.local.strategy'; 8 | 9 | @Module({ 10 | imports: [AdminModule, PassportModule], 11 | controllers: [AuthAdminController], 12 | providers: [AuthService, JwtAdminStrategy, AdminStrategy] 13 | }) 14 | export class AuthModule {} 15 | -------------------------------------------------------------------------------- /src/apis/auth/auth.swagger.ts: -------------------------------------------------------------------------------- 1 | import { OkResponse } from '@app/base'; 2 | import { applyDecorators } from '@nestjs/common'; 3 | import { ApiOperation, getSchemaPath } from '@nestjs/swagger'; 4 | import { AdminEntity } from '../admin/entities/admin.entity'; 5 | import { UserType } from './interfaces/auth.interface'; 6 | 7 | const getRef = (userType: UserType) => { 8 | let $ref; 9 | 10 | switch (userType) { 11 | case 'admin': 12 | $ref = AdminEntity; 13 | break; 14 | } 15 | 16 | return $ref; 17 | }; 18 | 19 | const loginResponse = (userType: UserType) => ({ 20 | properties: { 21 | result: { 22 | type: 'array', 23 | items: { 24 | properties: { 25 | user: { 26 | $ref: getSchemaPath(getRef(userType)) 27 | }, 28 | accessToken: { example: 'string' } 29 | } 30 | } 31 | } 32 | } 33 | }); 34 | 35 | export function ApiRefreshToken(userType: UserType) { 36 | return applyDecorators( 37 | ApiOperation({ summary: 'Refresh token for ' + userType }), 38 | OkResponse(null, false, loginResponse(userType)) 39 | ); 40 | } 41 | 42 | export function ApiLogin(userType: UserType) { 43 | return applyDecorators( 44 | ApiOperation({ summary: 'Login for ' + userType }), 45 | OkResponse(null, false, loginResponse(userType)) 46 | ); 47 | } 48 | 49 | export function ApiChangePassword(userType: UserType) { 50 | return applyDecorators( 51 | ApiOperation({ summary: 'Change password for ' + userType }), 52 | OkResponse(getRef(userType)) 53 | ); 54 | } 55 | 56 | export function ApiLogout(userType: UserType) { 57 | return applyDecorators( 58 | ApiOperation({ summary: 'Logout for ' + userType }), 59 | OkResponse(null, false, { example: { message: 'Logout successfully' } }) 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/apis/auth/controllers/auth.admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { StrategyKey } from '@app/constants'; 2 | import { Controller } from '@nestjs/common'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 5 | import { AuthService } from '../services/auth.service'; 6 | import { AuthBaseController } from './auth.base.controller'; 7 | 8 | @ApiTags('Auth API For Admin') 9 | @Controller('/auth/admin') 10 | export class AuthAdminController extends AuthBaseController( 11 | 'admin', 12 | StrategyKey.LOCAL.ADMIN 13 | ) { 14 | constructor(public readonly authService: AuthService) { 15 | super(authService); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/apis/auth/controllers/auth.base.controller.ts: -------------------------------------------------------------------------------- 1 | import { AuthAdmin } from '@app/decorators/auth-admin.decorator'; 2 | import { User } from '@app/decorators/user.decorator'; 3 | import { Body, Get, HttpCode, Post, Req, Res, UseGuards } from '@nestjs/common'; 4 | import { AuthGuard } from '@nestjs/passport'; 5 | import { Request, Response } from 'express'; 6 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 7 | import { ApiChangePassword, ApiLogin, ApiLogout, ApiRefreshToken } from '../auth.swagger'; 8 | import { ChangePasswordDto } from '../dtos/change-password.dto'; 9 | import { LoginDto } from '../dtos/login.dto'; 10 | import { UserType } from '../interfaces/auth.interface'; 11 | import { AuthService } from '../services/auth.service'; 12 | 13 | export const AuthBaseController = ( 14 | userType: UserType, 15 | strategyKey: string 16 | ) => { 17 | class BaseController { 18 | constructor(public readonly authService: AuthService) {} 19 | 20 | @Post('login') 21 | @HttpCode(200) 22 | @ApiLogin(userType) 23 | @UseGuards(AuthGuard(strategyKey)) 24 | async login( 25 | @Body() _login: LoginDto, // Load to Swagger 26 | @User() userData: Entity, 27 | @Res({ passthrough: true }) response: Response 28 | ) { 29 | return this.authService.login(userData, response); 30 | } 31 | 32 | @Get('refresh-token') 33 | @ApiRefreshToken(userType) 34 | async refreshToken(@Req() request: Request) { 35 | return this.authService.refreshToken(request, userType); 36 | } 37 | 38 | @Post('change-password') 39 | @HttpCode(200) 40 | @ApiChangePassword(userType) 41 | @AuthAdmin() 42 | async changePassword(@Body() body: ChangePasswordDto, @User() user: Entity) { 43 | return this.authService.changePassword(body, user, userType); 44 | } 45 | 46 | @Get('logout') 47 | @HttpCode(200) 48 | @ApiLogout(userType) 49 | @AuthAdmin() 50 | async logout(@User() user: Entity) { 51 | return this.authService.logout(user); 52 | } 53 | } 54 | 55 | return BaseController; 56 | }; 57 | -------------------------------------------------------------------------------- /src/apis/auth/dtos/change-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class ChangePasswordDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | oldPassword!: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | newPassword!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/apis/auth/dtos/login-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumberString, IsString } from 'class-validator'; 2 | 3 | export class LoginAdminDto { 4 | @IsNumberString() 5 | phone!: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | password!: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/apis/auth/dtos/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumberString, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsNumberString() 5 | phone!: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | password!: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/apis/auth/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | import { AdminEntity } from '../../admin/entities/admin.entity'; 2 | 3 | export type UserType = 'admin'; 4 | 5 | export type User = AdminEntity; 6 | -------------------------------------------------------------------------------- /src/apis/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { CryptoService } from '@app/crypto'; 2 | import { SetCookieRFToken } from '@app/helpers/setCookieRFToken'; 3 | import { JwtService } from '@app/jwt'; 4 | import { RedisService } from '@app/redis'; 5 | import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; 6 | import * as argon2 from 'argon2'; 7 | import { Request, Response } from 'express'; 8 | import { AdminService } from 'src/apis/admin/services/admin.service'; 9 | import { ChangePasswordDto } from '../dtos/change-password.dto'; 10 | import { User, UserType } from '../interfaces/auth.interface'; 11 | 12 | @Injectable() 13 | export class AuthService { 14 | constructor( 15 | private readonly adminService: AdminService, 16 | private readonly jwtService: JwtService, 17 | private readonly redisService: RedisService, 18 | private readonly cryptoService: CryptoService 19 | ) {} 20 | 21 | login(user: User, response: Response) { 22 | const { id } = user; 23 | const payload = { id }; 24 | 25 | // Generate accessToken 26 | const accessToken = this.jwtService.signJwt(payload); 27 | const refreshToken = this.jwtService.signJwt(payload, true); 28 | 29 | //Cache token 30 | this.redisService.setRefreshToken(id, refreshToken); 31 | this.redisService.setAccessToken(id, accessToken); 32 | 33 | //Encrypt cookie 34 | const encryptId = this.cryptoService.encryptData(id); 35 | SetCookieRFToken(response, encryptId); 36 | const result = { user, accessToken }; 37 | return result; 38 | } 39 | 40 | async refreshToken(request: Request, userType: UserType) { 41 | const { sub } = request.cookies; 42 | 43 | const decryptData = this.cryptoService.decryptData(sub); 44 | const refreshToken = await this.redisService.getRefreshToken(decryptData); 45 | // Get Token from refresh token 46 | const user = await this.getUser(refreshToken, userType); 47 | const { id } = user; 48 | const accessToken = this.jwtService.signJwt({ id }); 49 | const result = { user, accessToken }; 50 | return result; 51 | } 52 | 53 | async getUser(refreshToken: string, userType: UserType) { 54 | const { id } = await this.jwtService.verifyJwt(refreshToken); 55 | const where = { id }; 56 | const targetServices = this.getService(userType); 57 | const user = await targetServices.getOne(where); 58 | if (!user) { 59 | throw new NotFoundException('User not found'); 60 | } 61 | return user; 62 | } 63 | 64 | async changePassword(input: ChangePasswordDto, user: User, userType: UserType) { 65 | const { newPassword, oldPassword } = input; 66 | const comparePassword = await argon2.verify(user.password, oldPassword); 67 | if (!comparePassword) { 68 | throw new BadRequestException({ oldPassword: 'Invalid old password' }); 69 | } 70 | const hashedPassword = await argon2.hash(newPassword); 71 | const targetServices = this.getService(userType); 72 | return targetServices.updateById(user.id, { password: hashedPassword }); 73 | } 74 | 75 | async logout(user: User) { 76 | const sub = user.id; 77 | this.redisService.delRFToken(sub); 78 | this.redisService.delAccessToken(sub); 79 | const result = { message: 'Logout successfully' }; 80 | return result; 81 | } 82 | 83 | getService(userType: UserType) { 84 | let targetServices: AdminService; 85 | switch (userType) { 86 | case 'admin': 87 | targetServices = this.adminService; 88 | break; 89 | } 90 | return targetServices; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/apis/auth/strategies/jwt/admin.jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { StrategyKey } from '@app/constants'; 2 | import { JwtPayload } from '@app/jwt'; 3 | import { RedisService } from '@app/redis'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { PassportStrategy } from '@nestjs/passport'; 6 | import { ExtractJwt, Strategy } from 'passport-jwt'; 7 | import { AdminService } from 'src/apis/admin/services/admin.service'; 8 | 9 | @Injectable() 10 | export class JwtAdminStrategy extends PassportStrategy(Strategy, StrategyKey.JWT.ADMIN) { 11 | constructor( 12 | private readonly redisService: RedisService, 13 | private readonly adminService: AdminService 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | ignoreExpiration: false, 18 | secretOrKey: process.env.SECRET_JWT 19 | }); 20 | } 21 | 22 | async validate(payload: JwtPayload) { 23 | const { id } = payload; 24 | const where = { id }; 25 | await this.redisService.getAccessToken(id); 26 | return this.adminService.getOneOrFail(where); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/apis/auth/strategies/local/admin.local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { StrategyKey } from '@app/constants'; 2 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import * as argon2 from 'argon2'; 5 | import { Strategy } from 'passport-local'; 6 | import { AdminService } from 'src/apis/admin/services/admin.service'; 7 | 8 | @Injectable() 9 | export class AdminStrategy extends PassportStrategy(Strategy, StrategyKey.LOCAL.ADMIN) { 10 | constructor(private readonly adminService: AdminService) { 11 | super({ 12 | usernameField: 'phone', 13 | passwordField: 'password' 14 | }); 15 | } 16 | 17 | async validate(phone: string, password: string) { 18 | const where = { phone }; 19 | const admin = await this.adminService.getOneOrFail(where); 20 | const comparePassword = await argon2.verify(admin.password, password); 21 | if (!comparePassword) { 22 | throw new UnauthorizedException('Invalid password'); 23 | } 24 | return admin; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/apis/chat/chat.gateway.ts: -------------------------------------------------------------------------------- 1 | import { TokenExpires } from '@app/constants'; 2 | import { JwtService } from '@app/jwt'; 3 | import { RedisService } from '@app/redis'; 4 | import { BadRequestException, UnauthorizedException } from '@nestjs/common'; 5 | import { 6 | OnGatewayConnection, 7 | OnGatewayDisconnect, 8 | OnGatewayInit, 9 | SubscribeMessage, 10 | WebSocketGateway, 11 | WebSocketServer 12 | } from '@nestjs/websockets'; 13 | import { Server, Socket } from 'socket.io'; 14 | import { AdminService } from '../admin/services/admin.service'; 15 | import { ChatMessage } from './dto/chat-message.dto'; 16 | import { MessageEntity } from './entities/message.entity'; 17 | import { MessageService } from './services/message.service'; 18 | import { RoomService } from './services/room.service'; 19 | 20 | @WebSocketGateway({ 21 | namespace: '/pooling', 22 | cors: { 23 | origin: '*' 24 | } 25 | }) 26 | export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { 27 | @WebSocketServer() server!: Server; 28 | 29 | constructor( 30 | private readonly redisService: RedisService, 31 | private readonly jwtService: JwtService, 32 | private readonly adminService: AdminService, 33 | private readonly roomService: RoomService, 34 | private readonly messageService: MessageService 35 | ) {} 36 | 37 | async handleDisconnect(client: Socket) { 38 | const user = await this.getUserData(client); 39 | this.redisService.del(`CHAT:${user.id}`); 40 | } 41 | 42 | async handleConnection(client: Socket, ..._args: any[]) { 43 | const user = await this.getUserData(client); 44 | this.redisService.set({ 45 | key: `CHAT:${user.id}`, 46 | value: client.id, 47 | expired: TokenExpires.redisRefreshToken 48 | }); 49 | } 50 | 51 | afterInit(_server: Server) { 52 | // console.log(server); 53 | } 54 | 55 | async getUserData(client: Socket) { 56 | const bearerToken = client.handshake.headers.authorization; 57 | const token = bearerToken?.split(' ')[1]; 58 | if (!token) { 59 | throw new UnauthorizedException('Missing access token'); 60 | } 61 | const { id } = await this.jwtService.verifyJwt(token); 62 | await this.redisService.getAccessToken(id); 63 | return this.adminService.getOneByIdOrFail(id); 64 | } 65 | 66 | @SubscribeMessage('sendMessage') 67 | async sendMessage(client: Socket, payload: ChatMessage): Promise { 68 | const { text, roomId } = payload; 69 | const user = await this.getUserData(client); 70 | if (!text || !roomId) { 71 | throw new BadRequestException('Missing payload'); 72 | } 73 | const room = await this.roomService.getOneByIdOrFail(roomId); 74 | const { members } = room; 75 | const socketIds: string[] = []; 76 | for (let i = 0; i < members.length; i++) { 77 | const userId = members[i]; 78 | const socketId = await this.redisService.get(`CHAT:${userId}`); 79 | if (socketId) { 80 | socketIds.push(socketId); 81 | } 82 | } 83 | const message = new MessageEntity(); 84 | message.roomId = roomId; 85 | message.user = user; 86 | message.text = text; 87 | const newMessage = await message.save(); 88 | socketIds.forEach((socketId) => { 89 | client.to(socketId).emit('receiveMessage', { 90 | ...newMessage, 91 | user: { 92 | ...user, 93 | password: undefined 94 | } 95 | }); 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/apis/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AdminModule } from '../admin/admin.module'; 4 | import { ChatGateway } from './chat.gateway'; 5 | import { MessageController } from './controllers/message.controller'; 6 | import { RoomController } from './controllers/room.controller'; 7 | import { MessageEntity } from './entities/message.entity'; 8 | import { RoomEntity } from './entities/room.entity'; 9 | import { MessageService } from './services/message.service'; 10 | import { RoomService } from './services/room.service'; 11 | 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([RoomEntity, MessageEntity]), AdminModule], 14 | controllers: [RoomController, MessageController], 15 | providers: [RoomService, ChatGateway, MessageService] 16 | }) 17 | export class ChatModule {} 18 | -------------------------------------------------------------------------------- /src/apis/chat/controllers/message.controller.ts: -------------------------------------------------------------------------------- 1 | import { PaginationDto } from '@app/base'; 2 | import { AuthAdmin } from '@app/decorators/auth-admin.decorator'; 3 | import { Controller, Get, Param, Query } from '@nestjs/common'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | import { MessageService } from '../services/message.service'; 6 | 7 | @Controller('message') 8 | @ApiTags('Message API') 9 | @AuthAdmin() 10 | export class MessageController { 11 | constructor(private readonly messageService: MessageService) {} 12 | 13 | @Get('/:roomId') 14 | getAllByRoom(@Param('roomId') roomId: string, @Query() query: PaginationDto) { 15 | return this.messageService.getAllWithPagination(query, { 16 | roomId 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/apis/chat/controllers/room.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiCreate, ApiUpdate, BaseController, PaginationDto } from '@app/base'; 2 | import { AuthAdmin } from '@app/decorators/auth-admin.decorator'; 3 | import { User } from '@app/decorators/user.decorator'; 4 | import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; 5 | import { ApiTags } from '@nestjs/swagger'; 6 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 7 | import { ArrayContains } from 'typeorm'; 8 | import { CreateRoomDto } from '../dto/create-room.dto'; 9 | import { UpdateRoomDto } from '../dto/update-room.dto'; 10 | import { RoomEntity } from '../entities/room.entity'; 11 | import { RoomService } from '../services/room.service'; 12 | 13 | @Controller('room') 14 | @ApiTags('Chat Room API') 15 | @AuthAdmin() 16 | export class RoomController extends BaseController(RoomEntity, 'room') { 17 | relations = []; 18 | 19 | constructor(private readonly roomService: RoomService) { 20 | super(roomService); 21 | } 22 | 23 | @Get('/all') 24 | getAllByUserId(@Query() query: PaginationDto, @User() user: AdminEntity) { 25 | return this.roomService.getAllWithPagination(query, { 26 | members: ArrayContains([user.id]) 27 | }); 28 | } 29 | 30 | @Post('create') 31 | @ApiCreate(RoomEntity, 'room') 32 | create(@Body() body: CreateRoomDto): Promise { 33 | return super.create(body); 34 | } 35 | 36 | @Post('update/:id') 37 | @ApiUpdate(RoomEntity, 'room') 38 | update(@Param('id') id: string, @Body() body: UpdateRoomDto): Promise { 39 | return super.update(id, body); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/apis/chat/dto/chat-message.dto.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | text: string; 3 | roomId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/apis/chat/dto/create-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class CreateMessageDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | roomId!: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | userId!: string; 11 | 12 | @IsString() 13 | @IsNotEmpty() 14 | text!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/apis/chat/dto/create-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsArray, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class CreateRoomDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | name!: string; 7 | 8 | @IsArray() 9 | @IsString({ each: true }) 10 | @IsNotEmpty({ each: true }) 11 | members!: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/apis/chat/dto/update-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateRoomDto } from './create-room.dto'; 3 | 4 | export class UpdateRoomDto extends PartialType(CreateRoomDto) {} 5 | -------------------------------------------------------------------------------- /src/apis/chat/entities/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from '@app/base'; 2 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 3 | import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; 4 | import { RoomEntity } from './room.entity'; 5 | 6 | @Entity({ name: 'message' }) 7 | export class MessageEntity extends BaseEntity { 8 | @Column() 9 | text!: string; 10 | 11 | @Column({ name: 'user_id' }) 12 | userId!: string; 13 | 14 | @Column({ name: 'room_id' }) 15 | roomId!: string; 16 | 17 | @ManyToOne(() => AdminEntity) 18 | @JoinColumn({ name: 'user_id' }) 19 | user?: AdminEntity; 20 | 21 | @ManyToOne(() => RoomEntity) 22 | @JoinColumn({ name: 'room_id' }) 23 | room?: RoomEntity; 24 | } 25 | -------------------------------------------------------------------------------- /src/apis/chat/entities/room.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from '@app/base'; 2 | import { Column, Entity } from 'typeorm'; 3 | 4 | @Entity({ name: 'room' }) 5 | export class RoomEntity extends BaseEntity { 6 | @Column() 7 | name!: string; 8 | 9 | @Column({ type: 'simple-array' }) 10 | members!: string[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/apis/chat/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from '@app/base'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { AdminService } from 'src/apis/admin/services/admin.service'; 5 | import { Repository } from 'typeorm'; 6 | import { CreateMessageDto } from '../dto/create-message.dto'; 7 | import { MessageEntity } from '../entities/message.entity'; 8 | import { RoomService } from './room.service'; 9 | 10 | @Injectable() 11 | export class MessageService extends BaseService { 12 | name = 'Message'; 13 | 14 | constructor( 15 | @InjectRepository(MessageEntity) 16 | messageRepo: Repository, 17 | private readonly adminService: AdminService, 18 | private readonly roomService: RoomService 19 | ) { 20 | super(messageRepo); 21 | } 22 | 23 | async create(input: CreateMessageDto) { 24 | const { userId, roomId } = input; 25 | await this.adminService.getOneByIdOrFail(userId); 26 | await this.roomService.getOneByIdOrFail(roomId); 27 | return this.repo.create(input).save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/apis/chat/services/room.service.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from '@app/base'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { AdminService } from 'src/apis/admin/services/admin.service'; 5 | import { Repository } from 'typeorm'; 6 | import { CreateRoomDto } from '../dto/create-room.dto'; 7 | import { RoomEntity } from '../entities/room.entity'; 8 | 9 | @Injectable() 10 | export class RoomService extends BaseService { 11 | name = 'Room'; 12 | 13 | constructor( 14 | @InjectRepository(RoomEntity) 15 | private readonly roomRepo: Repository, 16 | private readonly adminService: AdminService 17 | ) { 18 | super(roomRepo); 19 | } 20 | 21 | async create(input: CreateRoomDto) { 22 | const { members } = input; 23 | for (let i = 0; i < members.length; i++) { 24 | const adminId = members[i]; 25 | await this.adminService.getOneById(adminId); 26 | } 27 | return this.repo.create(input).save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/apis/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | HttpCode, 4 | ParseFilePipeBuilder, 5 | Post, 6 | Req, 7 | UploadedFile, 8 | UseInterceptors 9 | } from '@nestjs/common'; 10 | import { FileInterceptor } from '@nestjs/platform-express'; 11 | import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; 12 | import { Request } from 'express'; 13 | import { UploadService } from './upload.service'; 14 | 15 | @Controller('upload') 16 | @ApiTags('Upload API') 17 | export class UploadController { 18 | constructor(private readonly uploadService: UploadService) {} 19 | 20 | @Post() 21 | @HttpCode(200) 22 | @UseInterceptors(FileInterceptor('file')) 23 | @ApiConsumes('multipart/form-data') 24 | @ApiBody({ 25 | schema: { 26 | type: 'object', 27 | properties: { 28 | media: { 29 | type: 'string', 30 | format: 'binary' 31 | } 32 | } 33 | } 34 | }) 35 | uploadSingle( 36 | @Req() req: Request, 37 | @UploadedFile(new ParseFilePipeBuilder().build()) 38 | file: Express.Multer.File 39 | ) { 40 | return this.uploadService.uploadSingle(req, file); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/apis/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UploadController } from './upload.controller'; 3 | import { UploadService } from './upload.service'; 4 | 5 | @Module({ 6 | controllers: [UploadController], 7 | providers: [UploadService] 8 | }) 9 | export class UploadModule {} 10 | -------------------------------------------------------------------------------- /src/apis/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 4 | import { extname } from 'path'; 5 | 6 | @Injectable() 7 | export class UploadService { 8 | private checkDir() { 9 | const path = './uploads'; 10 | 11 | if (!existsSync(path)) { 12 | mkdirSync(path); 13 | } 14 | } 15 | 16 | uploadSingle(req: Request, file: Express.Multer.File) { 17 | this.checkDir(); 18 | 19 | const randomName = Array(32) 20 | .fill(null) 21 | .map(() => Math.round(Math.random() * 16).toString(16)) 22 | .join(''); 23 | 24 | const fileName = `${randomName}${extname(file.originalname)}`; 25 | const filePath = `./uploads/${fileName}`; 26 | 27 | try { 28 | writeFileSync(filePath, file.buffer); 29 | return fileName; 30 | } catch (err: any) { 31 | throw new ForbiddenException([ 32 | { 33 | field: 'file', 34 | message: err.message 35 | } 36 | ]); 37 | } 38 | } 39 | 40 | uploadMultiple(req: Request, files: Array) { 41 | this.checkDir(); 42 | 43 | const response: string[] = []; 44 | 45 | for (const file of files) { 46 | const randomName = Array(32) 47 | .fill(null) 48 | .map(() => Math.round(Math.random() * 16).toString(16)) 49 | .join(''); 50 | 51 | const fileName = `${randomName}${extname(file.originalname)}`; 52 | const filePath = `./uploads/${fileName}`; 53 | 54 | response.push(fileName); 55 | 56 | try { 57 | writeFileSync(filePath, file.buffer); 58 | } catch (err: any) { 59 | console.error(`Failed to save file: ${err.message}`); 60 | } 61 | } 62 | return response; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiExcludeController, ApiTags } from '@nestjs/swagger'; 3 | import { AppService } from './app.service'; 4 | 5 | @Controller() 6 | @ApiTags('App') 7 | @ApiExcludeController() 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Get() 12 | getHello(): string { 13 | return this.appService.getHello(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CryptoModule } from '@app/crypto'; 2 | import { DatabaseModule } from '@app/database'; 3 | import { JWTModule } from '@app/jwt'; 4 | import { RedisModule } from '@app/redis'; 5 | import { Module } from '@nestjs/common'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | import { DevtoolsModule } from '@nestjs/devtools-integration'; 8 | import { MulterModule } from '@nestjs/platform-express'; 9 | import { ServeStaticModule } from '@nestjs/serve-static'; 10 | import { join } from 'path'; 11 | import { ApiModule } from 'src/apis/api.module'; 12 | import { AppController } from './app.controller'; 13 | import { providers } from './app.provider'; 14 | import { AppService } from './app.service'; 15 | 16 | @Module({ 17 | imports: [ 18 | ConfigModule.forRoot({ 19 | isGlobal: true, 20 | envFilePath: `.env.${process.env.NODE_ENV}` 21 | }), 22 | DevtoolsModule.register({ 23 | http: process.env.NODE_ENV !== 'production' 24 | }), 25 | MulterModule.register({ 26 | dest: './upload' 27 | }), 28 | ServeStaticModule.forRoot({ 29 | rootPath: join(__dirname, '..', '..', '..', 'uploads') 30 | }), 31 | DatabaseModule, 32 | JWTModule, 33 | RedisModule, 34 | CryptoModule, 35 | ApiModule 36 | ], 37 | controllers: [AppController], 38 | providers: [AppService, ...providers] 39 | }) 40 | export class AppModule {} 41 | -------------------------------------------------------------------------------- /src/app/app.provider.ts: -------------------------------------------------------------------------------- 1 | import { HttpExceptionFilter } from '@app/filters/http-exception.filter'; 2 | import { TypeormExceptionFilter } from '@app/filters/typeorm-exception.filter'; 3 | import { ResponseTransformInterceptor } from '@app/interceptors/response-transform.interceptor'; 4 | import { 5 | BadRequestException, 6 | ClassSerializerInterceptor, 7 | Provider, 8 | ValidationError, 9 | ValidationPipe 10 | } from '@nestjs/common'; 11 | import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; 12 | 13 | const exceptionFactory = (errors: ValidationError[]) => { 14 | throw new BadRequestException( 15 | errors.reduce((prev, next) => { 16 | const err = validationErrors(next); 17 | 18 | return { 19 | ...prev, 20 | ...err 21 | }; 22 | }, {}) 23 | ); 24 | }; 25 | 26 | const validationErrors = (err: ValidationError) => { 27 | if (!err.constraints && err.children && err.children.length > 0) { 28 | return validationErrors(err.children[0]); 29 | } 30 | 31 | return { 32 | [err.property]: err.constraints ? Object.values(err.constraints)[0] : '' 33 | }; 34 | }; 35 | 36 | export const providers: Provider[] = [ 37 | { 38 | provide: APP_FILTER, 39 | useClass: HttpExceptionFilter 40 | }, 41 | { 42 | provide: APP_FILTER, 43 | useClass: TypeormExceptionFilter 44 | }, 45 | { 46 | provide: APP_INTERCEPTOR, 47 | useClass: ResponseTransformInterceptor 48 | }, 49 | { 50 | provide: APP_INTERCEPTOR, 51 | useClass: ClassSerializerInterceptor 52 | }, 53 | { 54 | provide: APP_PIPE, 55 | useValue: new ValidationPipe({ 56 | exceptionFactory 57 | }) 58 | } 59 | ]; 60 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app.swagger.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Logger } from '@nestjs/common'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | import { AdminEntity } from 'src/apis/admin/entities/admin.entity'; 4 | 5 | export function useSwagger(app: INestApplication) { 6 | const logger = new Logger('Swagger'); 7 | const port = process.env.PORT || 3000; 8 | const path = 'docs'; 9 | const config = new DocumentBuilder() 10 | .setTitle('NestJS Example') 11 | .setDescription('NestJS Example Documentation') 12 | .setVersion('1.0') 13 | .addBearerAuth() 14 | .build(); 15 | const document = SwaggerModule.createDocument(app, config, { 16 | extraModels 17 | }); 18 | SwaggerModule.setup(path, app, document, { 19 | swaggerOptions: { 20 | tagsSorter: 'alpha', 21 | operationsSorter: (a, b) => { 22 | const methodsOrder = ['get', 'post', 'put', 'patch', 'delete', 'options', 'trace']; 23 | let result = 24 | methodsOrder.indexOf(a.get('method')) - methodsOrder.indexOf(b.get('method')); 25 | 26 | if (result === 0) { 27 | result = a.get('path').localeCompare(b.get('path')); 28 | } 29 | 30 | return result; 31 | } 32 | } 33 | }); 34 | logger.log(`Your documentation is running on http://localhost:${port}/${path}`); 35 | } 36 | 37 | const extraModels = [AdminEntity]; 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import * as cookieParser from 'cookie-parser'; 3 | import { AppModule } from './app/app.module'; 4 | import { useSwagger } from './app/app.swagger'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule, { 8 | snapshot: true 9 | }); 10 | 11 | app.setGlobalPrefix('api'); 12 | app.enableCors({ 13 | origin: '*' 14 | }); 15 | app.use(cookieParser()); 16 | useSwagger(app); 17 | 18 | const port = process.env.PORT || 3000; 19 | await app.listen(port); 20 | } 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule] 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | }, 13 | "moduleNameMapper": { 14 | "@app/jwt/(.*)": "/../libs/jwt/src/$1", 15 | "@app/jwt": "/../libs/jwt/src", 16 | "@app/crypto/(.*)": "/../libs/crypto/src/$1", 17 | "@app/crypto": "/../libs/crypto/src", 18 | "@app/redis/(.*)": "/../libs/redis/src/$1", 19 | "@app/redis": "/../libs/redis/src", 20 | "@app/database/(.*)": "/../libs/database/src/$1", 21 | "@app/database": "/../libs/database/src", 22 | "@app/base/(.*)": "/../libs/base/src/$1", 23 | "@app/base": "/../libs/base/src", 24 | "@app/decorators/(.*)": "/../libs/decorators/src/$1", 25 | "@app/decorators": "/../libs/decorators/src", 26 | "@app/filters/(.*)": "/../libs/filters/src/$1", 27 | "@app/filters": "/../libs/filters/src", 28 | "@app/guards/(.*)": "/../libs/guards/src/$1", 29 | "@app/guards": "/../libs/guards/src", 30 | "@app/interceptors/(.*)": "/../libs/interceptors/src/$1", 31 | "@app/interceptors": "/../libs/interceptors/src", 32 | "@app/helpers/(.*)": "/../libs/helpers/src/$1", 33 | "@app/helpers": "/../libs/helpers/src", 34 | "@app/enums/(.*)": "/../libs/enums/src/$1", 35 | "@app/enums": "/../libs/enums/src", 36 | "@app/interfaces/(.*)": "/../libs/interfaces/src/$1", 37 | "@app/interfaces": "/../libs/interfaces/src", 38 | "@app/constants/(.*)": "/../libs/constants/src/$1", 39 | "@app/constants": "/../libs/constants/src" 40 | } 41 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "strict": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "skipLibCheck": true, 18 | "strictNullChecks": true, 19 | "strictBindCallApply": false, 20 | "noImplicitAny": false, 21 | "forceConsistentCasingInFileNames": false, 22 | "noFallthroughCasesInSwitch": false, 23 | "paths": { 24 | "@app/jwt": [ 25 | "libs/jwt/src" 26 | ], 27 | "@app/jwt/*": [ 28 | "libs/jwt/src/*" 29 | ], 30 | "@app/crypto": [ 31 | "libs/crypto/src" 32 | ], 33 | "@app/crypto/*": [ 34 | "libs/crypto/src/*" 35 | ], 36 | "@app/redis": [ 37 | "libs/redis/src" 38 | ], 39 | "@app/redis/*": [ 40 | "libs/redis/src/*" 41 | ], 42 | "@app/database": [ 43 | "libs/database/src" 44 | ], 45 | "@app/database/*": [ 46 | "libs/database/src/*" 47 | ], 48 | "@app/base": [ 49 | "libs/base/src" 50 | ], 51 | "@app/base/*": [ 52 | "libs/base/src/*" 53 | ], 54 | "@app/decorators": [ 55 | "libs/decorators/src" 56 | ], 57 | "@app/decorators/*": [ 58 | "libs/decorators/src/*" 59 | ], 60 | "@app/filters": [ 61 | "libs/filters/src" 62 | ], 63 | "@app/filters/*": [ 64 | "libs/filters/src/*" 65 | ], 66 | "@app/guards": [ 67 | "libs/guards/src" 68 | ], 69 | "@app/guards/*": [ 70 | "libs/guards/src/*" 71 | ], 72 | "@app/interceptors": [ 73 | "libs/interceptors/src" 74 | ], 75 | "@app/interceptors/*": [ 76 | "libs/interceptors/src/*" 77 | ], 78 | "@app/helpers": [ 79 | "libs/helpers/src" 80 | ], 81 | "@app/helpers/*": [ 82 | "libs/helpers/src/*" 83 | ], 84 | "@app/enums": [ 85 | "libs/enums/src" 86 | ], 87 | "@app/enums/*": [ 88 | "libs/enums/src/*" 89 | ], 90 | "@app/interfaces": [ 91 | "libs/interfaces/src" 92 | ], 93 | "@app/interfaces/*": [ 94 | "libs/interfaces/src/*" 95 | ], 96 | "@app/constants": [ 97 | "libs/constants/src" 98 | ], 99 | "@app/constants/*": [ 100 | "libs/constants/src/*" 101 | ] 102 | } 103 | } 104 | } --------------------------------------------------------------------------------