├── .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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------