├── .dockerignore
├── .env.development
├── .env.example
├── .env.test
├── .eslintrc.js
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ └── ossar-analysis.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── Dockerfile
├── README.md
├── jest-e2e.json
├── jest.config.json
├── nest-cli.json
├── ormconfig.js
├── package.json
├── src
├── app.module.ts
├── application
│ ├── ports
│ │ ├── IRepository.ts
│ │ └── IUsersRepository.ts
│ └── use-cases
│ │ ├── PostsUseCases.ts
│ │ └── UsersUseCases.ts
├── domain
│ ├── exceptions
│ │ └── DomainException.ts
│ ├── models
│ │ ├── Post.ts
│ │ └── User.ts
│ ├── services
│ │ └── .gitkeep
│ └── shared
│ │ ├── IEntity.ts
│ │ └── IValueObject.ts
├── infrastructure
│ ├── cache
│ │ └── index.ts
│ ├── database
│ │ ├── mapper
│ │ │ ├── BaseEntity.ts
│ │ │ ├── PostEntity.ts
│ │ │ └── UserEntity.ts
│ │ ├── migrations
│ │ │ ├── 1590881548444-CreateUserAndPost.ts
│ │ │ └── 1590889425093-UpdateRelationship.ts
│ │ └── repositories
│ │ │ ├── BaseRepository.ts
│ │ │ └── UsersRepository.ts
│ ├── environments
│ │ └── index.ts
│ ├── ioc
│ │ ├── posts.module.ts
│ │ └── users.module.ts
│ ├── rest
│ │ ├── http-exception.filter.ts
│ │ ├── logging.interceptor.ts
│ │ └── validation.pipe.ts
│ └── terminus
│ │ └── index.ts
├── main.ts
└── presentation
│ ├── controllers
│ ├── PostsController.ts
│ └── UsersController.ts
│ ├── errors
│ ├── BadRequestError.ts
│ ├── NotFoundError.ts
│ └── UnprocessableEntityError.ts
│ └── view-models
│ ├── posts
│ ├── CreatePostVM.ts
│ └── PostVM.ts
│ └── users
│ ├── CreateUserVM.ts
│ └── UserVM.ts
├── test
├── e2e
│ ├── posts.e2e-spec.ts
│ └── users.e2e-spec.ts
└── unit
│ ├── application
│ ├── PostsUseCases.unit-spec.ts
│ └── UsersUseCases.unit-spec.ts
│ ├── domain
│ └── User.unit-spec.ts
│ └── presentation
│ ├── PostsController.unit-spec.ts
│ └── UsersController.unit-spec.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | .vscode
4 | coverage
5 | docker-compose.yml
6 | README.md
7 |
8 | # Node Files #
9 | node_modules
10 | npm-debug.log
11 | npm-debug.log.*
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | HOST=0.0.0.0
2 | PORT=8000
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Clean Architecture NestJS
2 | APP_DESCRIPTION=Sistema usando Clean Architecture com NestJS
3 | API_VERSION=v1
4 | HOST=0.0.0.0
5 | PORT=9000
6 | DB_CONNECTION=postgres
7 | DB_HOST=localhost
8 | DB_PORT=5432
9 | DB_USERNAME=postgres
10 | DB_PASSWORD=postgres
11 | DB_DATABASE=clean-architecture
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | HOST=0.0.0.0
2 | PORT=8000
3 | DB_DATABASE=clean-architecture-test
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const prettier = require('./prettier');
2 |
3 | module.exports = {
4 | parser: '@typescript-eslint/parser',
5 | parserOptions: {
6 | project: 'tsconfig.json',
7 | sourceType: 'module',
8 | },
9 | plugins: ['@typescript-eslint/eslint-plugin', 'eslint-plugin-import-helpers'],
10 | extends: [
11 | 'plugin:@typescript-eslint/eslint-recommended',
12 | 'plugin:@typescript-eslint/recommended',
13 | 'prettier',
14 | 'prettier/@typescript-eslint',
15 | ],
16 | root: true,
17 | env: {
18 | node: true,
19 | jest: true,
20 | },
21 | rules: {
22 | 'prettier/prettier': ['error', prettier],
23 | '@typescript-eslint/interface-name-prefix': 'off',
24 | '@typescript-eslint/explicit-function-return-type': 'off',
25 | '@typescript-eslint/no-explicit-any': 'off',
26 | '@typescript-eslint/no-empty-interface': 'off',
27 | '@typescript-eslint/no-namespace': 'off',
28 | 'import-helpers/order-imports': [
29 | 'warn',
30 | {
31 | newlinesBetween: 'always',
32 | groups: [
33 | 'module',
34 | [
35 | '/^application/',
36 | '/^presentation/',
37 | '/^domain/',
38 | '/^infrastructure/',
39 | ],
40 |
41 | ['parent', 'sibling', 'index'],
42 | ],
43 | alphabetize: { order: 'asc', ignoreCase: true },
44 | },
45 | ],
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '22 15 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/ossar-analysis.yml:
--------------------------------------------------------------------------------
1 | # This workflow integrates a collection of open source static analysis tools
2 | # with GitHub code scanning. For documentation, or to provide feedback, visit
3 | # https://github.com/github/ossar-action
4 | name: OSSAR
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | # The branches below must be a subset of the branches above
11 | branches: [ master ]
12 | schedule:
13 | - cron: '15 1 * * 5'
14 |
15 | jobs:
16 | OSSAR-Scan:
17 | # OSSAR runs on windows-latest.
18 | # ubuntu-latest and macos-latest support coming soon
19 | runs-on: windows-latest
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v2
24 |
25 | # Ensure a compatible version of dotnet is installed.
26 | # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
27 | # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action.
28 | # GitHub hosted runners already have a compatible version of dotnet installed and this step may be skipped.
29 | # For self-hosted runners, ensure dotnet version 3.1.201 or later is installed by including this action:
30 | # - name: Install .NET
31 | # uses: actions/setup-dotnet@v1
32 | # with:
33 | # dotnet-version: '3.1.x'
34 |
35 | # Run open source static analysis tools
36 | - name: Run OSSAR
37 | uses: github/ossar-action@v1
38 | id: ossar
39 |
40 | # Upload results to the Security tab
41 | - name: Upload OSSAR results
42 | uses: github/codeql-action/upload-sarif@v1
43 | with:
44 | sarif_file: ${{ steps.ossar.outputs.sarifFile }}
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 |
36 | .env
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote":true,
3 | "trailingComma":"all",
4 | "printWidth":80,
5 | "proseWrap":"never",
6 | "endOfLine":"lf",
7 | "semi": true
8 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "javascript.format.enable": false,
4 | "eslint.alwaysShowStatus": true,
5 | "eslint.options": {
6 | "extensions": [".html", ".ts", ".js", ".tsx"]
7 | },
8 | "eslint.packageManager": "yarn",
9 | "typescript.tsdk": "node_modules/typescript/lib",
10 | "editor.codeActionsOnSave": {
11 | "source.fixAll.eslint": true
12 | },
13 | "eslint.validate": [
14 | "javascript",
15 | "javascriptreact",
16 | "typescript",
17 | "typescriptreact"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.16-alpine
2 |
3 | WORKDIR /app
4 |
5 | ENV PATH /app/node_modules/.bin:$PATH
6 |
7 | COPY package.json ./
8 |
9 | RUN yarn --silent
10 |
11 | # COPY . .
12 | # EXPOSE 5000
13 |
14 | CMD "yarn start:prod"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
6 | [travis-url]: https://travis-ci.org/nestjs/nest
7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
8 | [linux-url]: https://travis-ci.org/nestjs/nest
9 |
10 | A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 | ## Description
28 |
29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
30 |
31 | ## Installation
32 |
33 | ```bash
34 | $ npm install
35 | ```
36 |
37 | ## Running the app
38 |
39 | ```bash
40 | # development
41 | $ npm run start
42 |
43 | # watch mode
44 | $ npm run start:dev
45 |
46 | # production mode
47 | $ npm run start:prod
48 | ```
49 |
50 | ## Test
51 |
52 | ```bash
53 | # unit tests
54 | $ npm run test
55 |
56 | # e2e tests
57 | $ npm run test:e2e
58 |
59 | # test coverage
60 | $ npm run test:cov
61 | ```
62 |
63 | ## Support
64 |
65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
66 |
67 | ## Stay in touch
68 |
69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
70 | - Website - [https://nestjs.com](https://nestjs.com/)
71 | - Twitter - [@nestframework](https://twitter.com/nestframework)
72 |
73 | ## License
74 |
75 | Nest is [MIT licensed](LICENSE).
76 |
--------------------------------------------------------------------------------
/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleDirectories": ["node_modules", "src"],
3 | "moduleFileExtensions": ["js", "json", "ts"],
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | },
9 | "moduleNameMapper": {
10 | "^@application(.*)$": "/src/application$1",
11 | "^@presentation(.*)$": "/src/presentation$1",
12 | "^@infrastructure(.*)$": "/src/infrastructure$1",
13 | "^@domain(.*)$": "/src/domain$1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleDirectories": ["node_modules", "src"],
3 | "moduleFileExtensions": ["js", "json", "ts"],
4 | "testEnvironment": "node",
5 | "testRegex": ".unit-spec.ts$",
6 | "transform": {
7 | "^.+\\.ts$": "ts-jest"
8 | },
9 | "coverageDirectory": "./coverage",
10 | "collectCoverage": true,
11 | "moduleNameMapper": {
12 | "^@application(.*)$": "/src/application$1",
13 | "^@presentation(.*)$": "/src/presentation$1",
14 | "^@infrastructure(.*)$": "/src/infrastructure$1",
15 | "^@domain(.*)$": "/src/domain$1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/ormconfig.js:
--------------------------------------------------------------------------------
1 | const connection = process.env.DB_CONNECTION;
2 | const host = process.env.DB_HOST;
3 | const port = process.env.DB_PORT;
4 | const username = process.env.DB_USERNAME;
5 | const password = process.env.DB_PASSWORD;
6 | const database = process.env.DB_DATABASE;
7 | const environment = process.env.NODE_ENV;
8 |
9 | module.exports = {
10 | type: connection,
11 | host: host,
12 | port: port,
13 | username: username,
14 | password: password,
15 | database: database,
16 |
17 | entities: ['dist/infrastructure/database/mapper/*.js'],
18 |
19 | synchronize: false,
20 |
21 | logging: true,
22 | logger: 'file',
23 |
24 | migrationsRun: environment === 'test', // I prefer to run manually in dev
25 | migrationsTableName: 'migrations',
26 | migrations: ['dist/infrastructure/database/migrations/*.js'],
27 | cli: {
28 | migrationsDir: 'src/infrastructure/database/migrations',
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-typeorm",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "MIT",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "prestart": "rimraf dist",
11 | "build": "nest build",
12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
13 | "start": "nest start",
14 | "start:dev": "cross-env NODE_ENV=development yarn start --watch",
15 | "start:dev:hrm": "cross-env NODE_ENV=development nest build --webpack --webpackPath webpack-hmr.config.js",
16 | "start:stage": "cross-env NODE_ENV=stage nest start --watch",
17 | "start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
18 | "start:prod": "cross-env NODE_ENV=production node dist/main",
19 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
20 | "test": "cross-env NODE_ENV=test jest",
21 | "test:watch": "cross-env NODE_ENV=test jest --watch",
22 | "test:cov": "cross-env NODE_ENV=test jest --coverage",
23 | "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
24 | "test:e2e": "cross-env NODE_ENV=test jest --config ./jest-e2e.json"
25 | },
26 | "dependencies": {
27 | "@godaddy/terminus": "^4.4.1",
28 | "@nestjs/common": "^7.0.0",
29 | "@nestjs/config": "^0.5.0",
30 | "@nestjs/core": "^7.0.0",
31 | "@nestjs/platform-express": "^7.0.0",
32 | "@nestjs/swagger": "^4.5.8",
33 | "@nestjs/terminus": "^7.0.1",
34 | "@nestjs/typeorm": "^7.1.0",
35 | "body-parser": "^1.19.0",
36 | "cache-manager": "^3.3.0",
37 | "chalk": "^4.0.0",
38 | "class-transformer": "^0.3.1",
39 | "class-validator": "^0.12.2",
40 | "compression": "^1.7.4",
41 | "cross-env": "^7.0.2",
42 | "express-rate-limit": "^5.1.3",
43 | "helmet": "^3.22.0",
44 | "pg": "^8.2.1",
45 | "reflect-metadata": "^0.1.13",
46 | "rimraf": "^3.0.2",
47 | "rxjs": "^6.5.4",
48 | "swagger-ui-express": "^4.1.4",
49 | "typeorm": "^0.2.25"
50 | },
51 | "devDependencies": {
52 | "@nestjs/cli": "^7.0.0",
53 | "@nestjs/schematics": "^7.0.0",
54 | "@nestjs/testing": "^7.0.0",
55 | "@types/compression": "^1.7.0",
56 | "@types/express": "^4.17.3",
57 | "@types/express-rate-limit": "^5.0.0",
58 | "@types/helmet": "^0.0.47",
59 | "@types/jest": "25.1.4",
60 | "@types/lodash": "^4.14.152",
61 | "@types/node": "^13.9.1",
62 | "@types/supertest": "^2.0.8",
63 | "@typescript-eslint/eslint-plugin": "^2.23.0",
64 | "@typescript-eslint/parser": "^2.23.0",
65 | "eslint": "^6.8.0",
66 | "eslint-config-prettier": "^6.10.0",
67 | "eslint-plugin-import": "^2.20.2",
68 | "eslint-plugin-import-helpers": "^1.0.2",
69 | "jest": "^25.1.0",
70 | "lodash": "^4.17.21",
71 | "prettier": "^1.19.1",
72 | "start-server-webpack-plugin": "^2.2.5",
73 | "supertest": "^4.0.2",
74 | "ts-jest": "25.2.1",
75 | "ts-loader": "^6.2.1",
76 | "ts-node": "^8.6.2",
77 | "tsconfig-paths": "^3.9.0",
78 | "typescript": "^3.7.4",
79 | "webpack-node-externals": "^1.7.2"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, CacheModule, CacheInterceptor } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import { APP_INTERCEPTOR } from '@nestjs/core';
4 | import { TerminusModule } from '@nestjs/terminus';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 |
7 | import { CacheService } from 'infrastructure/cache';
8 | import { setEnvironment } from 'infrastructure/environments';
9 | import { UsersModule } from 'infrastructure/ioc/users.module';
10 | import { PostsModule } from 'infrastructure/ioc/posts.module';
11 | import { HealthController } from 'infrastructure/terminus/index';
12 |
13 | @Module({
14 | imports: [
15 | UsersModule,
16 | PostsModule,
17 | ConfigModule.forRoot({
18 | isGlobal: true,
19 | expandVariables: true,
20 | envFilePath: setEnvironment(),
21 | }),
22 | TypeOrmModule.forRoot(),
23 | CacheModule.registerAsync({
24 | useClass: CacheService,
25 | }),
26 | TerminusModule,
27 | ],
28 | controllers: [HealthController],
29 | providers: [
30 | {
31 | provide: APP_INTERCEPTOR,
32 | useClass: CacheInterceptor,
33 | },
34 | ],
35 | })
36 | export class AppModule {}
37 |
--------------------------------------------------------------------------------
/src/application/ports/IRepository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | FindManyOptions,
4 | FindConditions,
5 | ObjectID,
6 | FindOneOptions,
7 | DeepPartial,
8 | SaveOptions,
9 | UpdateResult,
10 | DeleteResult,
11 | InsertResult,
12 | RemoveOptions,
13 | } from 'typeorm';
14 | import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
15 |
16 | @Injectable()
17 | export abstract class IRepository {
18 | abstract hasId(entity: Entity): boolean;
19 |
20 | abstract getId(entity: Entity): any;
21 |
22 | abstract create(): Entity;
23 |
24 | abstract create(entityLikeArray: DeepPartial[]): Entity[];
25 |
26 | abstract create(entityLike: DeepPartial): Entity;
27 |
28 | abstract create(
29 | plainEntityLikeOrPlainEntityLikes?:
30 | | DeepPartial
31 | | DeepPartial[],
32 | ): Entity | Entity[];
33 |
34 | abstract merge(
35 | mergeIntoEntity: Entity,
36 | ...entityLikes: DeepPartial[]
37 | ): Entity;
38 |
39 | abstract preload(
40 | entityLike: DeepPartial,
41 | ): Promise;
42 |
43 | abstract save>(
44 | entities: T[],
45 | options: SaveOptions & { reload: false },
46 | ): Promise;
47 |
48 | abstract save>(
49 | entities: T[],
50 | options?: SaveOptions,
51 | ): Promise<(T & Entity)[]>;
52 |
53 | abstract save>(
54 | entity: T,
55 | options: SaveOptions & { reload: false },
56 | ): Promise;
57 |
58 | abstract save>(
59 | entity: T,
60 | options?: SaveOptions,
61 | ): Promise;
62 |
63 | abstract save>(
64 | entityOrEntities: T | T[],
65 | options?: SaveOptions,
66 | ): Promise;
67 |
68 | abstract remove(
69 | entities: Entity[],
70 | options?: RemoveOptions,
71 | ): Promise;
72 |
73 | abstract remove(entity: Entity, options?: RemoveOptions): Promise;
74 |
75 | abstract remove(
76 | entityOrEntities: Entity | Entity[],
77 | options?: RemoveOptions,
78 | ): Promise;
79 |
80 | abstract softRemove>(
81 | entities: T[],
82 | options: SaveOptions & { reload: false },
83 | ): Promise;
84 |
85 | abstract softRemove>(
86 | entities: T[],
87 | options?: SaveOptions,
88 | ): Promise<(T & Entity)[]>;
89 |
90 | abstract softRemove>(
91 | entity: T,
92 | options: SaveOptions & { reload: false },
93 | ): Promise;
94 |
95 | abstract softRemove>(
96 | entity: T,
97 | options?: SaveOptions,
98 | ): Promise;
99 |
100 | abstract softRemove>(
101 | entityOrEntities: T | T[],
102 | options?: SaveOptions,
103 | ): Promise;
104 |
105 | abstract recover>(
106 | entities: T[],
107 | options: SaveOptions & { reload: false },
108 | ): Promise;
109 |
110 | abstract recover>(
111 | entities: T[],
112 | options?: SaveOptions,
113 | ): Promise<(T & Entity)[]>;
114 |
115 | abstract recover>(
116 | entity: T,
117 | options: SaveOptions & { reload: false },
118 | ): Promise;
119 |
120 | abstract recover>(
121 | entity: T,
122 | options?: SaveOptions,
123 | ): Promise;
124 |
125 | abstract recover>(
126 | entityOrEntities: T | T[],
127 | options?: SaveOptions,
128 | ): Promise;
129 |
130 | abstract insert(
131 | entity: QueryDeepPartialEntity | QueryDeepPartialEntity[],
132 | ): Promise;
133 |
134 | abstract update(
135 | criteria:
136 | | string
137 | | string[]
138 | | number
139 | | number[]
140 | | Date
141 | | Date[]
142 | | ObjectID
143 | | ObjectID[]
144 | | FindConditions,
145 | partialEntity: QueryDeepPartialEntity,
146 | ): Promise;
147 |
148 | abstract delete(
149 | criteria:
150 | | string
151 | | string[]
152 | | number
153 | | number[]
154 | | Date
155 | | Date[]
156 | | ObjectID
157 | | ObjectID[]
158 | | FindConditions,
159 | ): Promise;
160 |
161 | abstract softDelete(
162 | criteria:
163 | | string
164 | | string[]
165 | | number
166 | | number[]
167 | | Date
168 | | Date[]
169 | | ObjectID
170 | | ObjectID[]
171 | | FindConditions,
172 | ): Promise;
173 |
174 | abstract restore(
175 | criteria:
176 | | string
177 | | string[]
178 | | number
179 | | number[]
180 | | Date
181 | | Date[]
182 | | ObjectID
183 | | ObjectID[]
184 | | FindConditions,
185 | ): Promise;
186 |
187 | abstract count(options?: FindManyOptions): Promise;
188 |
189 | abstract count(conditions?: FindConditions): Promise;
190 |
191 | abstract count(
192 | optionsOrConditions?: FindManyOptions | FindConditions,
193 | ): Promise;
194 |
195 | abstract find(options?: FindManyOptions): Promise;
196 |
197 | abstract find(conditions?: FindConditions): Promise;
198 |
199 | abstract find(
200 | optionsOrConditions?: FindManyOptions | FindConditions,
201 | ): Promise;
202 |
203 | abstract findAndCount(
204 | options?: FindManyOptions,
205 | ): Promise<[Entity[], number]>;
206 |
207 | abstract findAndCount(
208 | conditions?: FindConditions,
209 | ): Promise<[Entity[], number]>;
210 |
211 | abstract findAndCount(
212 | optionsOrConditions?: FindManyOptions | FindConditions,
213 | ): Promise<[Entity[], number]>;
214 |
215 | abstract findByIds(
216 | ids: any[],
217 | options?: FindManyOptions,
218 | ): Promise;
219 |
220 | abstract findByIds(
221 | ids: any[],
222 | conditions?: FindConditions,
223 | ): Promise;
224 |
225 | abstract findByIds(
226 | ids: any[],
227 | optionsOrConditions?: FindManyOptions | FindConditions,
228 | ): Promise;
229 |
230 | abstract findOne(
231 | id?: string | number | Date | ObjectID,
232 | options?: FindOneOptions,
233 | ): Promise;
234 |
235 | abstract findOne(
236 | options?: FindOneOptions,
237 | ): Promise;
238 |
239 | abstract findOne(
240 | conditions?: FindConditions,
241 | options?: FindOneOptions,
242 | ): Promise;
243 |
244 | abstract findOne(
245 | optionsOrConditions?:
246 | | string
247 | | number
248 | | Date
249 | | ObjectID
250 | | FindOneOptions
251 | | FindConditions,
252 | maybeOptions?: FindOneOptions,
253 | ): Promise;
254 |
255 | abstract findOneOrFail(
256 | id?: string | number | Date | ObjectID,
257 | options?: FindOneOptions,
258 | ): Promise;
259 |
260 | abstract findOneOrFail(options?: FindOneOptions): Promise;
261 |
262 | abstract findOneOrFail(
263 | conditions?: FindConditions,
264 | options?: FindOneOptions,
265 | ): Promise;
266 |
267 | abstract findOneOrFail(
268 | optionsOrConditions?:
269 | | string
270 | | number
271 | | Date
272 | | ObjectID
273 | | FindOneOptions
274 | | FindConditions,
275 | maybeOptions?: FindOneOptions,
276 | ): Promise;
277 |
278 | abstract query(query: string, parameters?: any[]): Promise;
279 |
280 | abstract clear(): Promise;
281 |
282 | abstract increment(
283 | conditions: FindConditions,
284 | propertyPath: string,
285 | value: number | string,
286 | ): Promise;
287 |
288 | abstract decrement(
289 | conditions: FindConditions,
290 | propertyPath: string,
291 | value: number | string,
292 | ): Promise;
293 |
294 | abstract transaction(operation: () => Promise): Promise;
295 | }
296 |
--------------------------------------------------------------------------------
/src/application/ports/IUsersRepository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | import { User } from 'domain/models/User';
4 |
5 | import { IRepository } from './IRepository';
6 |
7 | @Injectable()
8 | export abstract class IUsersRepository extends IRepository {}
9 |
--------------------------------------------------------------------------------
/src/application/use-cases/PostsUseCases.ts:
--------------------------------------------------------------------------------
1 | import { Post } from 'domain/models/Post';
2 | import { Injectable, Logger, NotFoundException } from '@nestjs/common';
3 |
4 | import { IUsersRepository } from 'application/ports/IUsersRepository';
5 |
6 | @Injectable()
7 | export class PostsUseCases {
8 | private readonly logger = new Logger(PostsUseCases.name);
9 |
10 | constructor(private readonly usersRepository: IUsersRepository) {}
11 |
12 | async getAllPostsByUser(userId: number): Promise {
13 | this.logger.log('Fetch all user`s posts');
14 |
15 | const user = await this.usersRepository.findOne(userId, {
16 | relations: ['posts'],
17 | });
18 |
19 | if (!user)
20 | throw new NotFoundException(`The user {${userId}} wasn't found.`);
21 |
22 | return user.findPosts();
23 | }
24 |
25 | async getPostByUser(userId: number, postId: number): Promise {
26 | const user = await this.usersRepository.findOne(userId, {
27 | relations: ['posts'],
28 | });
29 |
30 | if (!user)
31 | throw new NotFoundException(`The user {${userId}} wasn't found.`);
32 |
33 | const post = user.findPost(postId);
34 |
35 | if (!post)
36 | throw new NotFoundException(`The post {${postId}} wasn't found.`);
37 |
38 | return post;
39 | }
40 |
41 | async createPost(userId: number, post: Post): Promise {
42 | const user = await this.usersRepository.findOne(userId, {
43 | relations: ['posts'],
44 | });
45 |
46 | if (!user)
47 | throw new NotFoundException(`The user {${userId}} wasn't found.`);
48 |
49 | user.createPost(post);
50 |
51 | const savedUser = await this.usersRepository.save(user);
52 |
53 | return savedUser.posts.find(p => p.title === post.title);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/application/use-cases/UsersUseCases.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common';
2 |
3 | import { IUsersRepository } from 'application/ports/IUsersRepository';
4 | import { User } from 'domain/models/User';
5 |
6 | @Injectable()
7 | export class UsersUseCases {
8 | private readonly logger = new Logger(UsersUseCases.name);
9 |
10 | constructor(private readonly usersRepository: IUsersRepository) {}
11 |
12 | async getUsers(): Promise {
13 | this.logger.log('Find all users');
14 |
15 | return await this.usersRepository.find({ loadEagerRelations: true });
16 | }
17 |
18 | async getUserById(id: number): Promise {
19 | this.logger.log(`Find the user: ${id}`);
20 |
21 | const user = await this.usersRepository.findOne(id);
22 | if (!user) throw new NotFoundException(`The user {${id}} has not found.`);
23 |
24 | return user;
25 | }
26 |
27 | async createUser(user: User): Promise {
28 | this.logger.log(`Saving a user`);
29 | return await this.usersRepository.save(user);
30 | }
31 |
32 | async updateUser(user: User): Promise {
33 | this.logger.log(`Updating a user: ${user.id}`);
34 | const result = await this.usersRepository.update({ id: user.id }, user);
35 |
36 | return result.affected > 0;
37 | }
38 |
39 | async deleteUser(id: number): Promise {
40 | this.logger.log(`Deleting a user: ${id}`);
41 | const result = await this.usersRepository.delete({ id });
42 |
43 | return result.affected > 0;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/domain/exceptions/DomainException.ts:
--------------------------------------------------------------------------------
1 | export class DomainException extends Error {
2 | constructor(message: string) {
3 | super(message);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/domain/models/Post.ts:
--------------------------------------------------------------------------------
1 | import { User } from 'domain/models/User';
2 | import { IEntity } from 'domain/shared/IEntity';
3 |
4 | export class Post implements IEntity {
5 | id?: number;
6 |
7 | title: string;
8 |
9 | text: string;
10 |
11 | user: User;
12 |
13 | createdAt?: Date;
14 |
15 | updatedAt?: Date;
16 |
17 | constructor(title: string, text: string, user?: User, id?: number) {
18 | this.title = title;
19 | this.text = text;
20 | this.user = user;
21 | this.id = id;
22 | }
23 |
24 | equals(entity: IEntity): boolean {
25 | if (!(entity instanceof Post)) return false;
26 |
27 | return this.id === entity.id;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/domain/models/User.ts:
--------------------------------------------------------------------------------
1 | import { Post } from './Post';
2 | import { DomainException } from 'domain/exceptions/DomainException';
3 | import { IEntity } from 'domain/shared/IEntity';
4 |
5 | export class User implements IEntity {
6 | id?: number;
7 |
8 | name: string;
9 |
10 | email: string;
11 |
12 | posts?: Post[];
13 |
14 | createdAt?: Date;
15 |
16 | updatedAt?: Date;
17 |
18 | constructor(name: string, email: string, posts?: Post[], id?: number) {
19 | this.name = name;
20 | this.email = email;
21 | this.posts = posts;
22 | this.id = id;
23 | }
24 |
25 | findPost(postId: number): Post {
26 | return this.posts?.find(p => p.id === postId) ?? null;
27 | }
28 |
29 | findPosts(): Post[] {
30 | return this.posts ?? [];
31 | }
32 |
33 | createPost(post: Post): void {
34 | if (!this.posts) this.posts = new Array();
35 |
36 | if (this.posts.map(p => p.title).includes(post.title))
37 | throw new DomainException('Post with the same name already exists');
38 |
39 | this.posts.push(post);
40 | }
41 |
42 | equals(entity: IEntity) {
43 | if (!(entity instanceof User)) return false;
44 |
45 | return this.id === entity.id;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/domain/services/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hvpaiva/clean-architecture-nestjs/af4aa283b539c30eb8a9a99494ab08fc6d72720d/src/domain/services/.gitkeep
--------------------------------------------------------------------------------
/src/domain/shared/IEntity.ts:
--------------------------------------------------------------------------------
1 | import { DomainException } from 'domain/exceptions/DomainException';
2 | export interface IEntity {
3 | equals(entity: IEntity): boolean;
4 | }
5 |
--------------------------------------------------------------------------------
/src/domain/shared/IValueObject.ts:
--------------------------------------------------------------------------------
1 | export interface IValueObject {
2 | equals(valueObject: IValueObject): boolean;
3 | }
4 |
--------------------------------------------------------------------------------
/src/infrastructure/cache/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CacheOptionsFactory,
4 | CacheModuleOptions,
5 | } from '@nestjs/common';
6 |
7 | @Injectable()
8 | export class CacheService implements CacheOptionsFactory {
9 | createCacheOptions(): CacheModuleOptions {
10 | return {
11 | ttl: 5, // seconds
12 | max: 10, // maximum number of items in cache
13 | };
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/infrastructure/database/mapper/BaseEntity.ts:
--------------------------------------------------------------------------------
1 | import { EntitySchemaColumnOptions } from 'typeorm';
2 |
3 | export const BaseEntity = {
4 | id: {
5 | type: Number,
6 | primary: true,
7 | generated: true,
8 | } as EntitySchemaColumnOptions,
9 | createdAt: {
10 | name: 'created_at',
11 | type: 'timestamp with time zone',
12 | createDate: true,
13 | } as EntitySchemaColumnOptions,
14 | updatedAt: {
15 | name: 'updated_at',
16 | type: 'timestamp with time zone',
17 | updateDate: true,
18 | } as EntitySchemaColumnOptions,
19 | };
20 |
--------------------------------------------------------------------------------
/src/infrastructure/database/mapper/PostEntity.ts:
--------------------------------------------------------------------------------
1 | import { EntitySchema } from 'typeorm';
2 |
3 | import { Post } from 'domain/models/Post';
4 |
5 | import { BaseEntity } from './BaseEntity';
6 | import { User } from 'domain/models/User';
7 | import { UserEntity } from './UserEntity';
8 |
9 | export const PostEntity = new EntitySchema({
10 | name: 'Post',
11 | tableName: 'posts',
12 | target: Post,
13 | columns: {
14 | ...BaseEntity,
15 | title: {
16 | type: String,
17 | length: 50,
18 | },
19 | text: {
20 | type: String,
21 | },
22 | },
23 | orderBy: {
24 | createdAt: 'ASC',
25 | },
26 | relations: {
27 | user: {
28 | type: 'many-to-one',
29 | target: () => User,
30 | joinColumn: true,
31 | },
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/src/infrastructure/database/mapper/UserEntity.ts:
--------------------------------------------------------------------------------
1 | import { EntitySchema } from 'typeorm';
2 |
3 | import { User } from 'domain/models/User';
4 |
5 | import { BaseEntity } from './BaseEntity';
6 | import { Post } from 'domain/models/Post';
7 |
8 | export const UserEntity = new EntitySchema({
9 | name: 'User',
10 | tableName: 'users',
11 | target: User,
12 | columns: {
13 | ...BaseEntity,
14 | name: {
15 | type: String,
16 | length: 100,
17 | },
18 | email: {
19 | type: String,
20 | length: 100,
21 | },
22 | },
23 | orderBy: {
24 | createdAt: 'ASC',
25 | },
26 | relations: {
27 | posts: {
28 | type: 'one-to-many',
29 | target: () => Post,
30 | cascade: ['insert', 'update'],
31 | onDelete: 'CASCADE',
32 | inverseSide: 'user',
33 | },
34 | },
35 | indices: [
36 | {
37 | name: 'IDX_USERS',
38 | unique: true,
39 | columns: ['name', 'email'],
40 | },
41 | ],
42 | uniques: [
43 | {
44 | name: 'UNIQUE_USERS',
45 | columns: ['email'],
46 | },
47 | ],
48 | });
49 |
--------------------------------------------------------------------------------
/src/infrastructure/database/migrations/1590881548444-CreateUserAndPost.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner} from "typeorm";
2 |
3 | export class CreateUserAndPost1590881548444 implements MigrationInterface {
4 | name = 'CreateUserAndPost1590881548444'
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`CREATE TABLE "posts" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(50) NOT NULL, "text" character varying NOT NULL, "userId" integer, CONSTRAINT "PK_2829ac61eff60fcec60d7274b9e" PRIMARY KEY ("id"))`);
8 | await queryRunner.query(`CREATE TABLE "users" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "email" character varying(100) NOT NULL, CONSTRAINT "UNIQUE_USERS" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`);
9 | await queryRunner.query(`CREATE UNIQUE INDEX "IDX_USERS" ON "users" ("name", "email") `);
10 | await queryRunner.query(`ALTER TABLE "posts" ADD CONSTRAINT "FK_ae05faaa55c866130abef6e1fee" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
11 | }
12 |
13 | public async down(queryRunner: QueryRunner): Promise {
14 | await queryRunner.query(`ALTER TABLE "posts" DROP CONSTRAINT "FK_ae05faaa55c866130abef6e1fee"`);
15 | await queryRunner.query(`DROP INDEX "IDX_USERS"`);
16 | await queryRunner.query(`DROP TABLE "users"`);
17 | await queryRunner.query(`DROP TABLE "posts"`);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/infrastructure/database/migrations/1590889425093-UpdateRelationship.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner} from "typeorm";
2 |
3 | export class UpdateRelationship1590889425093 implements MigrationInterface {
4 | name = 'UpdateRelationship1590889425093'
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`ALTER TABLE "posts" DROP CONSTRAINT "FK_ae05faaa55c866130abef6e1fee"`);
8 | await queryRunner.query(`ALTER TABLE "posts" ADD CONSTRAINT "FK_ae05faaa55c866130abef6e1fee" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
9 | }
10 |
11 | public async down(queryRunner: QueryRunner): Promise {
12 | await queryRunner.query(`ALTER TABLE "posts" DROP CONSTRAINT "FK_ae05faaa55c866130abef6e1fee"`);
13 | await queryRunner.query(`ALTER TABLE "posts" ADD CONSTRAINT "FK_ae05faaa55c866130abef6e1fee" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/infrastructure/database/repositories/BaseRepository.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ObjectLiteral,
3 | EntityManager,
4 | QueryRunner,
5 | DeepPartial,
6 | SaveOptions,
7 | RemoveOptions,
8 | InsertResult,
9 | ObjectID,
10 | FindConditions,
11 | UpdateResult,
12 | DeleteResult,
13 | FindManyOptions,
14 | FindOneOptions,
15 | EntitySchema,
16 | Connection,
17 | } from 'typeorm';
18 | import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
19 |
20 | export class BaseRepository {
21 | readonly manager: EntityManager;
22 | readonly queryRunner?: QueryRunner;
23 | readonly entitySchema: EntitySchema;
24 |
25 | constructor(connection: Connection, entity: EntitySchema) {
26 | this.queryRunner = connection.createQueryRunner();
27 | this.manager = this.queryRunner.manager;
28 | this.entitySchema = entity;
29 | }
30 |
31 | hasId(entity: Entity): boolean {
32 | return this.manager.hasId(entity);
33 | }
34 |
35 | getId(entity: Entity): any {
36 | return this.manager.getId(entity);
37 | }
38 |
39 | create(): Entity;
40 |
41 | create(entityLikeArray: DeepPartial[]): Entity[];
42 |
43 | create(entityLike: DeepPartial): Entity;
44 |
45 | create(
46 | plainEntityLikeOrPlainEntityLikes?:
47 | | DeepPartial
48 | | DeepPartial[],
49 | ): Entity | Entity[] {
50 | return this.manager.create(
51 | this.entitySchema as any,
52 | plainEntityLikeOrPlainEntityLikes as any,
53 | );
54 | }
55 |
56 | merge(
57 | mergeIntoEntity: Entity,
58 | ...entityLikes: DeepPartial[]
59 | ): Entity {
60 | return this.manager.merge(
61 | this.entitySchema as any,
62 | mergeIntoEntity,
63 | ...entityLikes,
64 | );
65 | }
66 |
67 | preload(entityLike: DeepPartial): Promise {
68 | return this.manager.preload(this.entitySchema as any, entityLike);
69 | }
70 |
71 | save>(
72 | entities: T[],
73 | options: SaveOptions & { reload: false },
74 | ): Promise;
75 |
76 | save>(
77 | entities: T[],
78 | options?: SaveOptions,
79 | ): Promise<(T & Entity)[]>;
80 |
81 | save>(
82 | entity: T,
83 | options: SaveOptions & { reload: false },
84 | ): Promise;
85 |
86 | save>(
87 | entity: T,
88 | options?: SaveOptions,
89 | ): Promise;
90 |
91 | save>(
92 | entityOrEntities: T | T[],
93 | options?: SaveOptions,
94 | ): Promise {
95 | return this.manager.save(
96 | this.entitySchema as any,
97 | entityOrEntities as any,
98 | options,
99 | );
100 | }
101 |
102 | remove(entities: Entity[], options?: RemoveOptions): Promise;
103 |
104 | remove(entity: Entity, options?: RemoveOptions): Promise;
105 |
106 | remove(
107 | entityOrEntities: Entity | Entity[],
108 | options?: RemoveOptions,
109 | ): Promise {
110 | return this.manager.remove(
111 | this.entitySchema as any,
112 | entityOrEntities as any,
113 | options,
114 | );
115 | }
116 |
117 | softRemove>(
118 | entities: T[],
119 | options: SaveOptions & { reload: false },
120 | ): Promise;
121 |
122 | softRemove>(
123 | entities: T[],
124 | options?: SaveOptions,
125 | ): Promise<(T & Entity)[]>;
126 |
127 | softRemove>(
128 | entity: T,
129 | options: SaveOptions & { reload: false },
130 | ): Promise;
131 |
132 | softRemove>(
133 | entity: T,
134 | options?: SaveOptions,
135 | ): Promise;
136 |
137 | softRemove>(
138 | entityOrEntities: T | T[],
139 | options?: SaveOptions,
140 | ): Promise {
141 | return this.manager.softRemove(
142 | this.entitySchema as any,
143 | entityOrEntities as any,
144 | options,
145 | );
146 | }
147 |
148 | recover>(
149 | entities: T[],
150 | options: SaveOptions & { reload: false },
151 | ): Promise;
152 |
153 | recover>(
154 | entities: T[],
155 | options?: SaveOptions,
156 | ): Promise<(T & Entity)[]>;
157 |
158 | recover>(
159 | entity: T,
160 | options: SaveOptions & { reload: false },
161 | ): Promise;
162 |
163 | recover>(
164 | entity: T,
165 | options?: SaveOptions,
166 | ): Promise;
167 |
168 | recover>(
169 | entityOrEntities: T | T[],
170 | options?: SaveOptions,
171 | ): Promise {
172 | return this.manager.recover(
173 | this.entitySchema as any,
174 | entityOrEntities as any,
175 | options,
176 | );
177 | }
178 |
179 | insert(
180 | entity: QueryDeepPartialEntity | QueryDeepPartialEntity[],
181 | ): Promise {
182 | return this.manager.insert(this.entitySchema as any, entity);
183 | }
184 |
185 | update(
186 | criteria:
187 | | string
188 | | string[]
189 | | number
190 | | number[]
191 | | Date
192 | | Date[]
193 | | ObjectID
194 | | ObjectID[]
195 | | FindConditions,
196 | partialEntity: QueryDeepPartialEntity,
197 | ): Promise {
198 | return this.manager.update(
199 | this.entitySchema as any,
200 | criteria as any,
201 | partialEntity,
202 | );
203 | }
204 |
205 | delete(
206 | criteria:
207 | | string
208 | | string[]
209 | | number
210 | | number[]
211 | | Date
212 | | Date[]
213 | | ObjectID
214 | | ObjectID[]
215 | | FindConditions,
216 | ): Promise {
217 | return this.manager.delete(this.entitySchema as any, criteria as any);
218 | }
219 |
220 | softDelete(
221 | criteria:
222 | | string
223 | | string[]
224 | | number
225 | | number[]
226 | | Date
227 | | Date[]
228 | | ObjectID
229 | | ObjectID[]
230 | | FindConditions,
231 | ): Promise {
232 | return this.manager.softDelete(this.entitySchema as any, criteria as any);
233 | }
234 |
235 | restore(
236 | criteria:
237 | | string
238 | | string[]
239 | | number
240 | | number[]
241 | | Date
242 | | Date[]
243 | | ObjectID
244 | | ObjectID[]
245 | | FindConditions,
246 | ): Promise {
247 | return this.manager.restore(this.entitySchema as any, criteria as any);
248 | }
249 |
250 | count(options?: FindManyOptions): Promise;
251 |
252 | count(conditions?: FindConditions): Promise;
253 |
254 | count(
255 | optionsOrConditions?: FindManyOptions | FindConditions,
256 | ): Promise {
257 | return this.manager.count(
258 | this.entitySchema as any,
259 | optionsOrConditions as any,
260 | );
261 | }
262 |
263 | find(options?: FindManyOptions): Promise;
264 |
265 | find(conditions?: FindConditions): Promise;
266 |
267 | find(
268 | optionsOrConditions?: FindManyOptions | FindConditions,
269 | ): Promise {
270 | return this.manager.find(
271 | this.entitySchema as any,
272 | optionsOrConditions as any,
273 | );
274 | }
275 |
276 | findAndCount(options?: FindManyOptions): Promise<[Entity[], number]>;
277 |
278 | findAndCount(
279 | conditions?: FindConditions,
280 | ): Promise<[Entity[], number]>;
281 |
282 | findAndCount(
283 | optionsOrConditions?: FindManyOptions | FindConditions,
284 | ): Promise<[Entity[], number]> {
285 | return this.manager.findAndCount(
286 | this.entitySchema as any,
287 | optionsOrConditions as any,
288 | );
289 | }
290 |
291 | findByIds(ids: any[], options?: FindManyOptions): Promise;
292 |
293 | findByIds(ids: any[], conditions?: FindConditions): Promise;
294 |
295 | findByIds(
296 | ids: any[],
297 | optionsOrConditions?: FindManyOptions | FindConditions,
298 | ): Promise {
299 | return this.manager.findByIds(
300 | this.entitySchema as any,
301 | ids,
302 | optionsOrConditions as any,
303 | );
304 | }
305 |
306 | findOne(
307 | id?: string | number | Date | ObjectID,
308 | options?: FindOneOptions,
309 | ): Promise;
310 |
311 | findOne(options?: FindOneOptions): Promise;
312 |
313 | findOne(
314 | conditions?: FindConditions,
315 | options?: FindOneOptions,
316 | ): Promise;
317 |
318 | findOne(
319 | optionsOrConditions?:
320 | | string
321 | | number
322 | | Date
323 | | ObjectID
324 | | FindOneOptions
325 | | FindConditions,
326 | maybeOptions?: FindOneOptions,
327 | ): Promise {
328 | return this.manager.findOne(
329 | this.entitySchema as any,
330 | optionsOrConditions as any,
331 | maybeOptions,
332 | );
333 | }
334 |
335 | findOneOrFail(
336 | id?: string | number | Date | ObjectID,
337 | options?: FindOneOptions,
338 | ): Promise;
339 |
340 | findOneOrFail(options?: FindOneOptions): Promise;
341 |
342 | findOneOrFail(
343 | conditions?: FindConditions,
344 | options?: FindOneOptions,
345 | ): Promise;
346 |
347 | findOneOrFail(
348 | optionsOrConditions?:
349 | | string
350 | | number
351 | | Date
352 | | ObjectID
353 | | FindOneOptions
354 | | FindConditions,
355 | maybeOptions?: FindOneOptions,
356 | ): Promise {
357 | return this.manager.findOneOrFail(
358 | this.entitySchema as any,
359 | optionsOrConditions as any,
360 | maybeOptions,
361 | );
362 | }
363 |
364 | query(query: string, parameters?: any[]): Promise {
365 | return this.manager.query(query, parameters);
366 | }
367 |
368 | clear(): Promise {
369 | return this.manager.clear(this.entitySchema);
370 | }
371 |
372 | increment(
373 | conditions: FindConditions,
374 | propertyPath: string,
375 | value: number | string,
376 | ): Promise {
377 | return this.manager.increment(
378 | this.entitySchema,
379 | conditions,
380 | propertyPath,
381 | value,
382 | );
383 | }
384 |
385 | decrement(
386 | conditions: FindConditions,
387 | propertyPath: string,
388 | value: number | string,
389 | ): Promise {
390 | return this.manager.decrement(
391 | this.entitySchema,
392 | conditions,
393 | propertyPath,
394 | value,
395 | );
396 | }
397 |
398 | async transaction(operation: () => Promise): Promise {
399 | await this.queryRunner.connect();
400 | await this.queryRunner.startTransaction();
401 |
402 | try {
403 | const result = await operation();
404 |
405 | await this.queryRunner.commitTransaction();
406 | return result;
407 | } catch (err) {
408 | await this.queryRunner.rollbackTransaction();
409 | } finally {
410 | await this.queryRunner.release();
411 | }
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/src/infrastructure/database/repositories/UsersRepository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectConnection } from '@nestjs/typeorm';
3 | import { Connection } from 'typeorm';
4 |
5 | import { IUsersRepository } from 'application/ports/IUsersRepository';
6 | import { User } from 'domain/models/User';
7 | import { UserEntity } from 'infrastructure/database/mapper/UserEntity';
8 |
9 | import { BaseRepository } from './BaseRepository';
10 |
11 | @Injectable()
12 | export class UsersRepository extends BaseRepository
13 | implements IUsersRepository {
14 | constructor(@InjectConnection() connection: Connection) {
15 | super(connection, UserEntity);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/infrastructure/environments/index.ts:
--------------------------------------------------------------------------------
1 | export function setEnvironment() {
2 | switch (process.env.NODE_ENV) {
3 | case 'test':
4 | return ['.env.test', '.env'];
5 | case 'stage':
6 | return ['.env.stage', '.env'];
7 | case 'development':
8 | return ['.env.development', '.env'];
9 | case 'production':
10 | default:
11 | return '.env';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/infrastructure/ioc/posts.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { IUsersRepository } from 'application/ports/IUsersRepository';
4 | import { UsersRepository } from 'infrastructure/database/repositories/UsersRepository';
5 | import { PostsController } from 'presentation/controllers/PostsController';
6 | import { PostsUseCases } from 'application/use-cases/PostsUseCases';
7 |
8 | @Module({
9 | imports: [],
10 | controllers: [PostsController],
11 | providers: [
12 | PostsUseCases,
13 | { provide: IUsersRepository, useClass: UsersRepository },
14 | ],
15 | })
16 | export class PostsModule {}
17 |
--------------------------------------------------------------------------------
/src/infrastructure/ioc/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { IUsersRepository } from 'application/ports/IUsersRepository';
4 | import { UsersUseCases } from 'application/use-cases/UsersUseCases';
5 | import { UsersRepository } from 'infrastructure/database/repositories/UsersRepository';
6 | import { UsersController } from 'presentation/controllers/UsersController';
7 |
8 | @Module({
9 | imports: [],
10 | controllers: [UsersController],
11 | providers: [
12 | UsersUseCases,
13 | { provide: IUsersRepository, useClass: UsersRepository },
14 | ],
15 | })
16 | export class UsersModule {}
17 |
--------------------------------------------------------------------------------
/src/infrastructure/rest/http-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | HttpException,
6 | HttpStatus,
7 | } from '@nestjs/common';
8 |
9 | /**
10 | * Http Error Filter.
11 | * Gets an HttpException in code and creates an error response
12 | */
13 | @Catch(HttpException)
14 | export class HttpExceptionFilter implements ExceptionFilter {
15 | catch(exception: HttpException, host: ArgumentsHost) {
16 | const ctx = host.switchToHttp();
17 | const response = ctx.getResponse();
18 | const request = ctx.getRequest();
19 | const statusCode = exception.getStatus();
20 |
21 | if (statusCode !== HttpStatus.UNPROCESSABLE_ENTITY)
22 | response.status(statusCode).json({
23 | statusCode,
24 | message: exception.message,
25 | timestamp: new Date().toISOString(),
26 | path: request.url,
27 | });
28 |
29 | const exceptionResponse: any = exception.getResponse();
30 | console.log(exceptionResponse);
31 |
32 | response.status(statusCode).json({
33 | statusCode,
34 | error: exceptionResponse.message,
35 | timestamp: new Date().toISOString(),
36 | path: request.url,
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/infrastructure/rest/logging.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CallHandler,
3 | ExecutionContext,
4 | Injectable,
5 | NestInterceptor,
6 | Logger,
7 | } from '@nestjs/common';
8 | import chalk from 'chalk';
9 | import { Observable } from 'rxjs';
10 | import { tap } from 'rxjs/operators';
11 |
12 | /**
13 | * Logger Interceptor.
14 | * Creates informative loggs to all requests, showing the path and
15 | * the method name.
16 | */
17 | @Injectable()
18 | export class LoggingInterceptor implements NestInterceptor {
19 | intercept(context: ExecutionContext, next: CallHandler): Observable {
20 | const parentType = chalk
21 | .hex('#87e8de')
22 | .bold(`${context.getArgs()[0].route.path}`);
23 | const fieldName = chalk
24 | .hex('#87e8de')
25 | .bold(`${context.getArgs()[0].route.stack[0].method}`);
26 | return next.handle().pipe(
27 | tap(() => {
28 | Logger.debug(`⛩ ${parentType} » ${fieldName}`, 'RESTful');
29 | }),
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/infrastructure/rest/validation.pipe.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | PipeTransform,
4 | ArgumentMetadata,
5 | UnprocessableEntityException,
6 | HttpException,
7 | HttpStatus,
8 | } from '@nestjs/common';
9 | import { plainToClass } from 'class-transformer';
10 | import { validate, ValidationError } from 'class-validator';
11 |
12 | interface IValidationError {
13 | property: string;
14 | errors: string[];
15 | constraints: {
16 | [type: string]: string;
17 | };
18 | }
19 |
20 | /**
21 | * Validation Pipe.
22 | * Gets Validation errors and creates custom error messages
23 | */
24 | @Injectable()
25 | export class ValidationPipe implements PipeTransform {
26 | async transform(value: any, { metatype }: ArgumentMetadata) {
27 | if (!metatype || !this.toValidate(metatype)) {
28 | return value;
29 | }
30 | const object = plainToClass(metatype, value);
31 | const errors = await validate(object);
32 | if (errors.length > 0) {
33 | throw new UnprocessableEntityException(this.formatErrors(errors));
34 | }
35 | return value;
36 | }
37 |
38 | private toValidate(metatype: Function): boolean {
39 | const types: Function[] = [String, Boolean, Number, Array, Object];
40 | return !types.includes(metatype);
41 | }
42 |
43 | private formatErrors(errors: ValidationError[]): IValidationError[] {
44 | return errors.map(err => {
45 | return {
46 | property: err.property,
47 | errors: Object.keys(err.constraints),
48 | constraints: err.constraints,
49 | };
50 | });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/infrastructure/terminus/index.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { ApiTags } from '@nestjs/swagger';
4 | import {
5 | HealthCheckService,
6 | DNSHealthIndicator,
7 | HealthCheck,
8 | TypeOrmHealthIndicator,
9 | } from '@nestjs/terminus';
10 |
11 | @ApiTags('Health Check')
12 | @Controller('health')
13 | export class HealthController {
14 | constructor(
15 | private health: HealthCheckService,
16 | private dns: DNSHealthIndicator,
17 | private db: TypeOrmHealthIndicator,
18 | private configService: ConfigService,
19 | ) {}
20 |
21 | @Get()
22 | @HealthCheck()
23 | healthCheck() {
24 | const host = this.configService.get('HOST');
25 | const port = this.configService.get('PORT');
26 | const urlApi = `http://${host}:${port}`; // TODO: Mudar para DNS
27 |
28 | return this.health.check([
29 | async () => this.db.pingCheck('database', { timeout: 300 }),
30 | () => this.dns.pingCheck('api', urlApi),
31 | ]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { NestFactory } from '@nestjs/core';
4 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
5 | import bodyParser from 'body-parser';
6 | import chalk from 'chalk';
7 | import compression from 'compression';
8 | import rateLimit from 'express-rate-limit';
9 | import helmet from 'helmet';
10 |
11 | import { HttpExceptionFilter } from 'infrastructure/rest/http-exception.filter';
12 | import { LoggingInterceptor } from 'infrastructure/rest/logging.interceptor';
13 | import { ValidationPipe } from 'infrastructure/rest/validation.pipe';
14 |
15 | import { AppModule } from './app.module';
16 |
17 | declare const module: any;
18 |
19 | async function bootstrap() {
20 | try {
21 | const app = await NestFactory.create(AppModule, {
22 | cors: true,
23 | });
24 | const configService = app.get(ConfigService);
25 | Logger.log(
26 | `Environment: ${chalk
27 | .hex('#87e8de')
28 | .bold(`${process.env.NODE_ENV?.toUpperCase()}`)}`,
29 | 'Bootstrap',
30 | );
31 |
32 | app.use(helmet());
33 | app.use(compression());
34 | app.use(bodyParser.json({ limit: '50mb' }));
35 | app.use(
36 | bodyParser.urlencoded({
37 | limit: '50mb',
38 | extended: true,
39 | parameterLimit: 50000,
40 | }),
41 | );
42 | app.use(
43 | rateLimit({
44 | windowMs: 1000 * 60 * 60,
45 | max: 1000, // 1000 requests por windowMs
46 | message:
47 | '⚠️ Too many request created from this IP, please try again after an hour',
48 | }),
49 | );
50 |
51 | // REST Global configurations
52 | app.useGlobalInterceptors(new LoggingInterceptor());
53 | app.useGlobalFilters(new HttpExceptionFilter());
54 | app.useGlobalPipes(new ValidationPipe());
55 |
56 | const APP_NAME = configService.get('APP_NAME');
57 | const APP_DESCRIPTION = configService.get('APP_DESCRIPTION');
58 | const API_VERSION = configService.get('API_VERSION', 'v1');
59 | const options = new DocumentBuilder()
60 | .setTitle(APP_NAME)
61 | .setDescription(APP_DESCRIPTION)
62 | .setVersion(API_VERSION)
63 | .build();
64 |
65 | const document = SwaggerModule.createDocument(app, options);
66 | SwaggerModule.setup('api', app, document);
67 | SwaggerModule.setup('/', app, document);
68 |
69 | Logger.log('Mapped {/, GET} Swagger api route', 'RouterExplorer');
70 | Logger.log('Mapped {/api, GET} Swagger api route', 'RouterExplorer');
71 |
72 | const HOST = configService.get('HOST', 'localhost');
73 | const PORT = configService.get('PORT', '3000');
74 |
75 | await app.listen(PORT);
76 | process.env.NODE_ENV !== 'production'
77 | ? Logger.log(
78 | `🚀 Server ready at http://${HOST}:${chalk
79 | .hex('#87e8de')
80 | .bold(`${PORT}`)}`,
81 | 'Bootstrap',
82 | false,
83 | )
84 | : Logger.log(
85 | `🚀 Server is listening on port ${chalk
86 | .hex('#87e8de')
87 | .bold(`${PORT}`)}`,
88 | 'Bootstrap',
89 | false,
90 | );
91 |
92 | if (module.hot) {
93 | module.hot.accept();
94 | module.hot.dispose(() => app.close());
95 | }
96 | } catch (error) {
97 | Logger.error(`❌ Error starting server, ${error}`, '', 'Bootstrap', false);
98 | process.exit();
99 | }
100 | }
101 | bootstrap().catch(e => {
102 | Logger.error(`❌ Error starting server, ${e}`, '', 'Bootstrap', false);
103 | throw e;
104 | });
105 |
--------------------------------------------------------------------------------
/src/presentation/controllers/PostsController.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Param, Get, Post, Body } from '@nestjs/common';
2 | import {
3 | ApiTags,
4 | ApiParam,
5 | ApiOperation,
6 | ApiOkResponse,
7 | ApiNotFoundResponse,
8 | ApiCreatedResponse,
9 | ApiBadRequestResponse,
10 | ApiUnprocessableEntityResponse,
11 | } from '@nestjs/swagger';
12 |
13 | import { PostsUseCases } from 'application/use-cases/PostsUseCases';
14 | import { NotFoundError } from 'presentation/errors/NotFoundError';
15 | import { BadRequestError } from 'presentation/errors/BadRequestError';
16 | import { UnprocessableEntityError } from 'presentation/errors/UnprocessableEntityError';
17 | import { PostVM } from 'presentation/view-models/posts/PostVM';
18 | import { CreatePostVM } from 'presentation/view-models/posts/CreatePostVM';
19 |
20 | @ApiTags('Posts')
21 | @Controller()
22 | export class PostsController {
23 | constructor(private readonly postsUseCases: PostsUseCases) {}
24 |
25 | @Get('users/:userId/posts')
26 | @ApiOperation({
27 | summary: 'Find all Posts of an User',
28 | })
29 | @ApiParam({
30 | name: 'userId',
31 | type: Number,
32 | description: 'The user id',
33 | })
34 | @ApiOkResponse({ description: 'Posts founded.', type: [PostVM] })
35 | @ApiNotFoundResponse({
36 | description: 'If the user passed in userId not exists.',
37 | type: NotFoundError,
38 | })
39 | async getPostsByUser(@Param('userId') userId: string): Promise {
40 | const posts = this.postsUseCases.getAllPostsByUser(parseInt(userId, 10));
41 |
42 | return (await posts).map(post => PostVM.toViewModel(post));
43 | }
44 |
45 | @Get('users/:userId/posts/:postId')
46 | @ApiOperation({
47 | summary: 'Find a Post of an User',
48 | })
49 | @ApiParam({
50 | name: 'userId',
51 | type: Number,
52 | description: 'The user id',
53 | })
54 | @ApiParam({
55 | name: 'postId',
56 | type: Number,
57 | description: 'The post id',
58 | })
59 | @ApiOkResponse({ description: 'Post founded.', type: PostVM })
60 | @ApiNotFoundResponse({
61 | description: 'If the user or the post not exists.',
62 | type: NotFoundError,
63 | })
64 | async getPost(
65 | @Param('userId') userId: string,
66 | @Param('postId') postId: string,
67 | ): Promise {
68 | const post = await this.postsUseCases.getPostByUser(
69 | parseInt(userId, 10),
70 | parseInt(postId, 10),
71 | );
72 |
73 | return PostVM.toViewModel(post);
74 | }
75 |
76 | @Post('users/:userId/posts')
77 | @ApiOperation({
78 | summary: 'Creates a Post',
79 | })
80 | @ApiCreatedResponse({ description: 'User created.', type: PostVM })
81 | @ApiBadRequestResponse({
82 | description: 'The request object doesn`t match the expected one',
83 | type: BadRequestError,
84 | })
85 | @ApiUnprocessableEntityResponse({
86 | description: 'Validation error while creating user',
87 | type: UnprocessableEntityError,
88 | })
89 | async createPost(
90 | @Param('userId') userId: string,
91 | @Body() createPost: CreatePostVM,
92 | ): Promise {
93 | const post = await this.postsUseCases.createPost(
94 | parseInt(userId, 10),
95 | CreatePostVM.fromViewModel(createPost),
96 | );
97 |
98 | return PostVM.toViewModel(post);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/presentation/controllers/UsersController.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Param, Get, Post, Body } from '@nestjs/common';
2 | import {
3 | ApiTags,
4 | ApiParam,
5 | ApiOperation,
6 | ApiCreatedResponse,
7 | ApiUnprocessableEntityResponse,
8 | ApiBadRequestResponse,
9 | ApiOkResponse,
10 | ApiNotFoundResponse,
11 | } from '@nestjs/swagger';
12 |
13 | import { UsersUseCases } from 'application/use-cases/UsersUseCases';
14 | import { CreateUserVM } from 'presentation/view-models/users/CreateUserVM';
15 | import { UserVM } from 'presentation/view-models/users/UserVM';
16 | import { BadRequestError } from 'presentation/errors/BadRequestError';
17 | import { UnprocessableEntityError } from 'presentation/errors/UnprocessableEntityError';
18 | import { NotFoundError } from 'presentation/errors/NotFoundError';
19 |
20 | @ApiTags('Users')
21 | @Controller('users')
22 | export class UsersController {
23 | constructor(private readonly usersUseCases: UsersUseCases) {}
24 |
25 | @Get(':id')
26 | @ApiOperation({
27 | summary: 'Find one user by id',
28 | })
29 | @ApiParam({
30 | name: 'id',
31 | type: Number,
32 | description: 'The user id',
33 | })
34 | @ApiOkResponse({ description: 'User founded.', type: UserVM })
35 | @ApiNotFoundResponse({
36 | description: 'User cannot be founded.',
37 | type: NotFoundError,
38 | })
39 | async get(@Param('id') id: string): Promise {
40 | const user = await this.usersUseCases.getUserById(parseInt(id, 10));
41 |
42 | return UserVM.toViewModel(user);
43 | }
44 |
45 | @Get()
46 | @ApiOperation({
47 | summary: 'Find all users',
48 | })
49 | @ApiOkResponse({ description: 'All user`s fetched.', type: [UserVM] })
50 | async getAll(): Promise {
51 | const users = await this.usersUseCases.getUsers();
52 |
53 | return users.map(user => UserVM.toViewModel(user));
54 | }
55 |
56 | @Post()
57 | @ApiOperation({
58 | summary: 'Creates an user',
59 | })
60 | @ApiCreatedResponse({ description: 'User created.', type: UserVM })
61 | @ApiBadRequestResponse({
62 | description: 'The request object doesn`t match the expected one',
63 | type: BadRequestError,
64 | })
65 | @ApiUnprocessableEntityResponse({
66 | description: 'Validation error while creating user',
67 | type: UnprocessableEntityError,
68 | })
69 | async createUser(@Body() createUser: CreateUserVM): Promise