├── .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 | Nest Logo 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 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 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 { 70 | const newUser = await this.usersUseCases.createUser( 71 | CreateUserVM.fromViewModel(createUser), 72 | ); 73 | 74 | return UserVM.toViewModel(newUser); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/presentation/errors/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class BadRequestError { 5 | @ApiProperty({ 6 | description: 'The error status.', 7 | example: HttpStatus.BAD_REQUEST, 8 | }) 9 | statusCode: HttpStatus; 10 | 11 | @ApiProperty({ 12 | description: 'The error message.', 13 | example: 'Unexpected token } in JSON at position 24', 14 | }) 15 | message: string; 16 | 17 | @ApiProperty({ 18 | description: 'The time of the executed error.', 19 | example: new Date(), 20 | }) 21 | timestamp: Date; 22 | 23 | @ApiProperty({ 24 | description: 'The REST path called.', 25 | example: '/users', 26 | }) 27 | path: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/presentation/errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class NotFoundError { 5 | @ApiProperty({ 6 | description: 'The error status.', 7 | example: HttpStatus.NOT_FOUND, 8 | }) 9 | statusCode: HttpStatus; 10 | 11 | @ApiProperty({ 12 | description: 'The error message.', 13 | example: 'The user {12} has not be found.', 14 | }) 15 | message: string; 16 | 17 | @ApiProperty({ 18 | description: 'The time of the executed error.', 19 | example: new Date(), 20 | }) 21 | timestamp: Date; 22 | 23 | @ApiProperty({ 24 | description: 'The REST path called.', 25 | example: '/users/12', 26 | }) 27 | path: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/presentation/errors/UnprocessableEntityError.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class UnprocessableEntityError { 5 | @ApiProperty({ 6 | description: 'The error status.', 7 | example: HttpStatus.UNPROCESSABLE_ENTITY, 8 | }) 9 | statusCode: HttpStatus; 10 | 11 | @ApiProperty({ 12 | description: 'The error validation error.', 13 | example: [ 14 | { 15 | property: 'name', 16 | errors: ['isNotEmpty'], 17 | constraints: { 18 | isNotEmpty: 'name should not be empty', 19 | }, 20 | }, 21 | { 22 | property: 'email', 23 | errors: ['isEmail', 'isNotEmpty'], 24 | constraints: { 25 | isEmail: 'email must be an email', 26 | isNotEmpty: 'email should not be empty', 27 | }, 28 | }, 29 | ], 30 | }) 31 | error: [ 32 | { 33 | property: string; 34 | errors: string[]; 35 | constraints: { 36 | [type: string]: string; 37 | }; 38 | }, 39 | ]; 40 | 41 | @ApiProperty({ 42 | description: 'The time of the executed error.', 43 | example: new Date(), 44 | }) 45 | timestamp: Date; 46 | 47 | @ApiProperty({ 48 | description: 'The REST path called.', 49 | example: '/users', 50 | }) 51 | path: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/presentation/view-models/posts/CreatePostVM.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsEmail } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | import { Post } from 'domain/models/Post'; 5 | 6 | export class CreatePostVM { 7 | @IsString() 8 | @IsNotEmpty() 9 | @ApiProperty({ 10 | description: 'The title of the post', 11 | example: 'Domain Driven Design', 12 | }) 13 | title: string; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | @ApiProperty({ 18 | description: 'The content of the post', 19 | example: 20 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', 21 | }) 22 | text: string; 23 | 24 | static fromViewModel(vm: CreatePostVM): Post { 25 | return new Post(vm.title, vm.text); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/presentation/view-models/posts/PostVM.ts: -------------------------------------------------------------------------------- 1 | import { Expose, plainToClass } from 'class-transformer'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Post } from 'domain/models/Post'; 4 | 5 | export class PostVM { 6 | @Expose() 7 | @ApiProperty({ 8 | description: 'The id of the post', 9 | example: 1, 10 | }) 11 | id: number; 12 | 13 | @Expose() 14 | @ApiProperty({ 15 | description: 'The title of the post', 16 | example: 'Domain Driven Design', 17 | }) 18 | title: string; 19 | 20 | @Expose() 21 | @ApiProperty({ 22 | description: 'The unique email of the user', 23 | example: 24 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', 25 | }) 26 | text: string; 27 | 28 | @Expose() 29 | @ApiProperty({ description: 'The crational date of the post' }) 30 | createdAt: Date; 31 | 32 | @Expose() 33 | @ApiProperty({ description: 'The date of the last post update' }) 34 | updatedAt: Date; 35 | 36 | static toViewModel(user: Post): PostVM { 37 | return plainToClass(PostVM, user, { excludeExtraneousValues: true }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/presentation/view-models/users/CreateUserVM.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString, IsEmail } from 'class-validator'; 3 | 4 | import { User } from 'domain/models/User'; 5 | 6 | export class CreateUserVM { 7 | @IsString() 8 | @IsNotEmpty() 9 | @ApiProperty({ 10 | description: 'The name of the user', 11 | example: 'John Doe', 12 | }) 13 | name: string; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | @IsEmail() 18 | @ApiProperty({ 19 | description: 'The unique email of the user', 20 | example: 'john.doe@gmail.com', 21 | }) 22 | email: string; 23 | 24 | static fromViewModel(vm: CreateUserVM): User { 25 | return new User(vm.name, vm.email); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/presentation/view-models/users/UserVM.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { plainToClass, Expose } from 'class-transformer'; 3 | 4 | import { User } from 'domain/models/User'; 5 | 6 | export class UserVM { 7 | @Expose() 8 | @ApiProperty({ 9 | description: 'The id of the user', 10 | example: 1, 11 | }) 12 | id: number; 13 | 14 | @Expose() 15 | @ApiProperty({ 16 | description: 'The name of the user', 17 | example: 'John Doe', 18 | }) 19 | name: string; 20 | 21 | @Expose() 22 | @ApiProperty({ 23 | description: 'The unique email of the user', 24 | example: 'john.doe@gmail.com', 25 | }) 26 | email: string; 27 | 28 | @Expose() 29 | @ApiProperty({ description: 'The crational date of the user' }) 30 | createdAt: Date; 31 | 32 | @Expose() 33 | @ApiProperty({ description: 'The date of the last user update' }) 34 | updatedAt: Date; 35 | 36 | static toViewModel(user: User): UserVM { 37 | return plainToClass(UserVM, user, { excludeExtraneousValues: true }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/e2e/posts.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, HttpStatus } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import request from 'supertest'; 4 | 5 | import { IUsersRepository } from 'application/ports/IUsersRepository'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | import { setEnvironment } from 'infrastructure/environments'; 9 | import { PostsModule } from 'infrastructure/ioc/posts.module'; 10 | import { User } from 'domain/models/User'; 11 | // import { User } from 'domain/models/User'; 12 | 13 | describe('Users', () => { 14 | let app: INestApplication; 15 | let usersRepository: IUsersRepository; 16 | 17 | beforeAll(async () => { 18 | const module = await Test.createTestingModule({ 19 | imports: [ 20 | PostsModule, 21 | TypeOrmModule.forRoot(), 22 | ConfigModule.forRoot({ 23 | isGlobal: true, 24 | expandVariables: true, 25 | envFilePath: setEnvironment(), 26 | }), 27 | ], 28 | }).compile(); 29 | 30 | app = module.createNestApplication(); 31 | await app.init(); 32 | usersRepository = module.get(IUsersRepository); 33 | }); 34 | 35 | it(`/POST users/:userId/posts`, async () => { 36 | await usersRepository.save(new User('John Doe', 'john.doe@gmail.com')); 37 | 38 | const { body } = await request(app.getHttpServer()) 39 | .post('/users/1/posts') 40 | .send({ title: 'Title', text: 'Text' }) 41 | .set('Accept', 'application/json') 42 | .expect('Content-Type', /json/) 43 | .expect(HttpStatus.CREATED); 44 | 45 | expect(body.id).toBeTruthy(); 46 | expect(body.createdAt).toBeTruthy(); 47 | expect(body.updatedAt).toBeTruthy(); 48 | 49 | expect(body).toEqual({ 50 | id: body.id, 51 | title: 'Title', 52 | text: 'Text', 53 | createdAt: body.createdAt, 54 | updatedAt: body.updatedAt, 55 | }); 56 | }); 57 | 58 | it(`/GET users/:userId/posts`, async () => { 59 | const { body } = await request(app.getHttpServer()) 60 | .get('/users/1/posts') 61 | .expect(HttpStatus.OK); 62 | 63 | expect(body).toHaveLength(1); 64 | expect(body[0].id).toBeTruthy(); 65 | expect(body[0].createdAt).toBeTruthy(); 66 | expect(body[0].updatedAt).toBeTruthy(); 67 | 68 | expect(body).toEqual([ 69 | { 70 | id: body[0].id, 71 | title: 'Title', 72 | text: 'Text', 73 | createdAt: body[0].createdAt, 74 | updatedAt: body[0].updatedAt, 75 | }, 76 | ]); 77 | }); 78 | 79 | it(`/GET users/:userId/posts/:postId`, async () => { 80 | const { body } = await request(app.getHttpServer()) 81 | .get('/users/1/posts/1') 82 | .expect(HttpStatus.OK); 83 | 84 | expect(body.id).toBeTruthy(); 85 | expect(body.createdAt).toBeTruthy(); 86 | expect(body.updatedAt).toBeTruthy(); 87 | 88 | expect(body).toEqual({ 89 | id: body.id, 90 | title: 'Title', 91 | text: 'Text', 92 | createdAt: body.createdAt, 93 | updatedAt: body.updatedAt, 94 | }); 95 | }); 96 | 97 | afterAll(async () => { 98 | await usersRepository.query(`DELETE FROM posts;`); 99 | await usersRepository.query(`DELETE FROM users;`); 100 | await usersRepository.query(`ALTER SEQUENCE users_id_seq RESTART WITH 1;`); 101 | await usersRepository.query(`ALTER SEQUENCE posts_id_seq RESTART WITH 1;`); 102 | await app.close(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/e2e/users.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, HttpStatus } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import request from 'supertest'; 4 | 5 | import { IUsersRepository } from 'application/ports/IUsersRepository'; 6 | import { UsersModule } from 'infrastructure/ioc/users.module'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import { setEnvironment } from 'infrastructure/environments'; 10 | 11 | describe('Users', () => { 12 | let app: INestApplication; 13 | let usersRepository: IUsersRepository; 14 | 15 | beforeAll(async () => { 16 | const module = await Test.createTestingModule({ 17 | imports: [ 18 | UsersModule, 19 | TypeOrmModule.forRoot(), 20 | ConfigModule.forRoot({ 21 | isGlobal: true, 22 | expandVariables: true, 23 | envFilePath: setEnvironment(), 24 | }), 25 | ], 26 | }).compile(); 27 | 28 | app = module.createNestApplication(); 29 | await app.init(); 30 | usersRepository = module.get(IUsersRepository); 31 | }); 32 | 33 | it(`/POST users`, async () => { 34 | const { body } = await request(app.getHttpServer()) 35 | .post('/users') 36 | .send({ name: 'John Doe', email: 'john.doe@gmail.com' }) 37 | .set('Accept', 'application/json') 38 | .expect('Content-Type', /json/) 39 | .expect(HttpStatus.CREATED); 40 | 41 | expect(body.id).toBeTruthy(); 42 | expect(body.createdAt).toBeTruthy(); 43 | expect(body.updatedAt).toBeTruthy(); 44 | 45 | expect(body).toEqual({ 46 | id: body.id, 47 | name: 'John Doe', 48 | email: 'john.doe@gmail.com', 49 | createdAt: body.createdAt, 50 | updatedAt: body.updatedAt, 51 | }); 52 | }); 53 | 54 | it(`/GET users`, async () => { 55 | const { body } = await request(app.getHttpServer()) 56 | .get('/users') 57 | .expect(HttpStatus.OK); 58 | 59 | expect(body).toHaveLength(1); 60 | expect(body[0].id).toBeTruthy(); 61 | expect(body[0].createdAt).toBeTruthy(); 62 | expect(body[0].updatedAt).toBeTruthy(); 63 | 64 | expect(body).toEqual([ 65 | { 66 | id: body[0].id, 67 | name: 'John Doe', 68 | email: 'john.doe@gmail.com', 69 | createdAt: body[0].createdAt, 70 | updatedAt: body[0].updatedAt, 71 | }, 72 | ]); 73 | }); 74 | 75 | it(`/GET users/:id`, async () => { 76 | const { body } = await request(app.getHttpServer()) 77 | .get('/users/1') 78 | .expect(HttpStatus.OK); 79 | 80 | expect(body.id).toBeTruthy(); 81 | expect(body.createdAt).toBeTruthy(); 82 | expect(body.updatedAt).toBeTruthy(); 83 | 84 | expect(body).toEqual({ 85 | id: body.id, 86 | name: 'John Doe', 87 | email: 'john.doe@gmail.com', 88 | createdAt: body.createdAt, 89 | updatedAt: body.updatedAt, 90 | }); 91 | }); 92 | 93 | afterAll(async () => { 94 | await usersRepository.query(`DELETE FROM users;`); 95 | await usersRepository.query(`ALTER SEQUENCE users_id_seq RESTART WITH 1;`); 96 | await app.close(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/unit/application/PostsUseCases.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { IUsersRepository } from 'application/ports/IUsersRepository'; 4 | import { PostsUseCases } from 'application/use-cases/PostsUseCases'; 5 | import { User } from 'domain/models/User'; 6 | import { NotFoundException } from '@nestjs/common'; 7 | import { Post } from 'domain/models/Post'; 8 | 9 | describe('PostsUseCases Test', () => { 10 | let usersRepository: IUsersRepository; 11 | let postsUseCases: PostsUseCases; 12 | 13 | const POST = new Post('Title', 'Text', null, 1); 14 | const POST2 = new Post('Title2', 'Text2', null, 1); 15 | const USER = new User('John Doe', 'john.doe@gmail.com', [POST], 1); 16 | const USER2 = new User('John Doe', 'john.doe@gmail.com', [POST, POST2], 1); 17 | 18 | beforeEach(async () => { 19 | const module = await Test.createTestingModule({ 20 | providers: [ 21 | PostsUseCases, 22 | { 23 | provide: IUsersRepository, 24 | useFactory: () => ({ 25 | save: jest.fn(() => true), 26 | findOne: jest.fn(() => true), 27 | find: jest.fn(() => true), 28 | update: jest.fn(() => true), 29 | delete: jest.fn(() => true), 30 | }), 31 | }, 32 | ], 33 | }).compile(); 34 | 35 | usersRepository = module.get(IUsersRepository); 36 | postsUseCases = module.get(PostsUseCases); 37 | }); 38 | 39 | it('shoud return a list of posts when the users have the posts in getAllPostsByUser', async () => { 40 | jest.spyOn(usersRepository, 'findOne').mockImplementation(async () => USER); 41 | const posts = await postsUseCases.getAllPostsByUser(1); 42 | 43 | expect(posts).toHaveLength(1); 44 | expect(posts).toStrictEqual([POST]); 45 | }); 46 | 47 | it('shoud return a empty list when user has no post in getAllPostsByUser', async () => { 48 | const user = new User('', '', null, 1); 49 | jest.spyOn(usersRepository, 'findOne').mockImplementation(async () => user); 50 | 51 | const posts = await postsUseCases.getAllPostsByUser(1); 52 | 53 | expect(posts).toHaveLength(0); 54 | expect(posts).toStrictEqual([]); 55 | }); 56 | 57 | it('shoud throw NotFoundException when the user is not found in getAllPostsByUser', async () => { 58 | try { 59 | jest 60 | .spyOn(usersRepository, 'findOne') 61 | .mockImplementation(async () => null); 62 | await postsUseCases.getAllPostsByUser(2); 63 | } catch (err) { 64 | expect(err instanceof NotFoundException).toBeTruthy(); 65 | expect(err.message).toBe("The user {2} wasn't found."); 66 | } 67 | }); 68 | 69 | it('shoud get a post when a valid user has a post in getPostByUser', async () => { 70 | jest.spyOn(usersRepository, 'findOne').mockImplementation(async () => USER); 71 | 72 | const post = await postsUseCases.getPostByUser(1, 1); 73 | 74 | expect(post instanceof Post).toBeTruthy(); 75 | expect(post).toStrictEqual(POST); 76 | }); 77 | 78 | it('shoud throw NotFoundException when not user is found in getPostByUser', async () => { 79 | try { 80 | jest 81 | .spyOn(usersRepository, 'findOne') 82 | .mockImplementation(async () => null); 83 | await postsUseCases.getPostByUser(2, 1); 84 | } catch (err) { 85 | expect(err instanceof NotFoundException).toBeTruthy(); 86 | expect(err.message).toBe("The user {2} wasn't found."); 87 | } 88 | }); 89 | 90 | it('shoud throw NotFoundException when user there are not post in getPostByUser', async () => { 91 | try { 92 | const user = new User('', '', null, 1); 93 | jest 94 | .spyOn(usersRepository, 'findOne') 95 | .mockImplementation(async () => user); 96 | await postsUseCases.getPostByUser(1, 1); 97 | } catch (err) { 98 | expect(err instanceof NotFoundException).toBeTruthy(); 99 | expect(err.message).toBe("The post {1} wasn't found."); 100 | } 101 | }); 102 | 103 | it('shoud throw NotFoundException when the post not exists in getPostByUser', async () => { 104 | try { 105 | jest 106 | .spyOn(usersRepository, 'findOne') 107 | .mockImplementation(async () => USER); 108 | await postsUseCases.getPostByUser(1, 99); 109 | } catch (err) { 110 | expect(err instanceof NotFoundException).toBeTruthy(); 111 | expect(err.message).toBe("The post {99} wasn't found."); 112 | } 113 | }); 114 | 115 | it('should create a post when user and post is valis in createPost', async () => { 116 | jest.spyOn(usersRepository, 'findOne').mockImplementation(async () => USER); 117 | jest.spyOn(usersRepository, 'save').mockImplementation(async () => USER2); 118 | 119 | const post = await postsUseCases.createPost(1, POST2); 120 | 121 | expect(post instanceof Post).toBeTruthy(); 122 | expect(post).toStrictEqual(POST2); 123 | }); 124 | 125 | it('should throw NotFoundException when the user not exists in createPost', async () => { 126 | try { 127 | jest 128 | .spyOn(usersRepository, 'findOne') 129 | .mockImplementation(async () => null); 130 | await postsUseCases.createPost(2, POST2); 131 | } catch (err) { 132 | expect(err instanceof NotFoundException).toBeTruthy(); 133 | expect(err.message).toBe("The user {2} wasn't found."); 134 | } 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/unit/application/UsersUseCases.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import { UpdateResult, DeleteResult } from 'typeorm'; 4 | 5 | import { IUsersRepository } from 'application/ports/IUsersRepository'; 6 | import { UsersUseCases } from 'application/use-cases/UsersUseCases'; 7 | import { User } from 'domain/models/User'; 8 | 9 | describe('UsersUseCases Test', () => { 10 | let usersRepository: IUsersRepository; 11 | let usersUseCases: UsersUseCases; 12 | 13 | const USER = new User('John Doe', 'john.doe@gmail.com', null, 1); 14 | const USER_OBJECT = { 15 | id: 1, 16 | name: 'John Doe', 17 | email: 'john.doe@gmail.com', 18 | posts: null, 19 | createdAt: new Date('2020-05-31T06:37:07.969Z'), 20 | updatedAt: new Date('2020-05-31T06:37:07.969Z'), 21 | } as User; 22 | 23 | beforeEach(async () => { 24 | const module = await Test.createTestingModule({ 25 | providers: [ 26 | UsersUseCases, 27 | { 28 | provide: IUsersRepository, 29 | useFactory: () => ({ 30 | save: jest.fn(() => true), 31 | findOne: jest.fn(() => true), 32 | find: jest.fn(() => true), 33 | update: jest.fn(() => true), 34 | delete: jest.fn(() => true), 35 | }), 36 | }, 37 | ], 38 | }).compile(); 39 | 40 | usersRepository = module.get(IUsersRepository); 41 | usersUseCases = module.get(UsersUseCases); 42 | }); 43 | 44 | it('should create a user when a valid user is passed in createUser', async () => { 45 | jest 46 | .spyOn(usersRepository, 'save') 47 | .mockImplementation(async () => USER_OBJECT); 48 | 49 | const user = await usersUseCases.createUser(USER_OBJECT); 50 | 51 | expect(user instanceof User); 52 | expect(user).toBe(USER_OBJECT); 53 | }); 54 | 55 | it('shoud get a user when a valid id is passed in getUserById', async () => { 56 | jest 57 | .spyOn(usersRepository, 'findOne') 58 | .mockImplementation(async () => USER_OBJECT); 59 | 60 | const user = await usersUseCases.getUserById(1); 61 | 62 | expect(user instanceof User); 63 | expect(user).toBe(USER_OBJECT); 64 | }); 65 | 66 | it('shoud throw NotFoundException when the user is not found in getUserById', async () => { 67 | try { 68 | jest 69 | .spyOn(usersRepository, 'findOne') 70 | .mockImplementation(async () => null); 71 | await usersUseCases.getUserById(2); 72 | } catch (err) { 73 | expect(err instanceof NotFoundException).toBeTruthy(); 74 | expect(err.message).toBe('The user {2} has not found.'); 75 | } 76 | }); 77 | 78 | it('shoud get all users in getUsers', async () => { 79 | jest 80 | .spyOn(usersRepository, 'find') 81 | .mockImplementation(async () => [USER_OBJECT]); 82 | const users = await usersUseCases.getUsers(); 83 | 84 | expect(users).toHaveLength(1); 85 | expect(users).toStrictEqual([USER_OBJECT]); 86 | }); 87 | 88 | it('shoud return true when user is updated in updateUser', async () => { 89 | jest 90 | .spyOn(usersRepository, 'update') 91 | .mockImplementation(async () => ({ affected: 1 } as UpdateResult)); 92 | const updated = await usersUseCases.updateUser(USER); 93 | 94 | expect(updated).toBeTruthy(); 95 | }); 96 | 97 | it('shoud return true when user is deleted in deleteUser', async () => { 98 | jest 99 | .spyOn(usersRepository, 'delete') 100 | .mockImplementation(async () => ({ affected: 1 } as DeleteResult)); 101 | const deleted = await usersUseCases.deleteUser(1); 102 | 103 | expect(deleted).toBeTruthy(); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/unit/domain/User.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'domain/models/User'; 2 | import { Post } from 'domain/models/Post'; 3 | import { DomainException } from 'domain/exceptions/DomainException'; 4 | 5 | describe('User tests', () => { 6 | let USER_MOCK: User; 7 | const USER_OBJECT = { name: 'John Doe', email: 'john.doe@gmail.com' }; 8 | 9 | let POST_MOCK: Post; 10 | const POST_OBJECT = { id: 1, title: 'Title', text: 'Text', user: null }; 11 | 12 | beforeEach(() => { 13 | POST_MOCK = new Post('Title', 'Text', null, 1); 14 | USER_MOCK = new User('John Doe', 'john.doe@gmail.com', null, 1); 15 | }); 16 | 17 | it('Should create an user', () => { 18 | const user = new User('John Doe', 'john.doe@gmail.com'); 19 | 20 | expect(user instanceof User).toBeTruthy(); 21 | expect(user).toEqual(USER_OBJECT); 22 | }); 23 | 24 | it('Should create a post', () => { 25 | const addicionalPost = new Post('Title2', 'Text2'); 26 | 27 | USER_MOCK.createPost(POST_MOCK); 28 | USER_MOCK.createPost(addicionalPost); 29 | 30 | expect(USER_MOCK.posts).toHaveLength(2); 31 | expect(USER_MOCK.posts).toEqual([POST_OBJECT, addicionalPost]); 32 | }); 33 | 34 | it('Should throw exception if a post with same name is created', () => { 35 | expect(() => { 36 | USER_MOCK.createPost(POST_MOCK); 37 | USER_MOCK.createPost(POST_MOCK); 38 | }).toThrow(DomainException); 39 | }); 40 | 41 | it('Should find a post', () => { 42 | USER_MOCK.createPost(POST_MOCK); 43 | 44 | const post = USER_MOCK.findPost(POST_MOCK.id); 45 | 46 | expect(post).toEqual(POST_OBJECT); 47 | }); 48 | 49 | it('Should not find a post', () => { 50 | const notFound = USER_MOCK.findPost(99); 51 | 52 | expect(notFound).toEqual(null); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/presentation/PostsController.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { PostsController } from 'presentation/controllers/PostsController'; 3 | import { PostsUseCases } from 'application/use-cases/PostsUseCases'; 4 | import { Post } from 'domain/models/Post'; 5 | import { PostVM } from 'presentation/view-models/posts/PostVM'; 6 | 7 | describe('PostsController Test', () => { 8 | let postsController: PostsController; 9 | let postsUseCases: PostsUseCases; 10 | 11 | const POST = new Post('Title', 'Text', null, 1); 12 | POST.createdAt = new Date('2020-05-31 02:20:58.037572-03'); 13 | POST.updatedAt = new Date('2020-05-31 02:20:58.037572-03'); 14 | 15 | const POST_VM = { 16 | id: 1, 17 | title: 'Title', 18 | text: 'Text', 19 | createdAt: new Date('2020-05-31 02:20:58.037572-03'), 20 | updatedAt: new Date('2020-05-31 02:20:58.037572-03'), 21 | } as PostVM; 22 | 23 | beforeEach(async () => { 24 | const module = await Test.createTestingModule({ 25 | providers: [ 26 | PostsController, 27 | { 28 | provide: PostsUseCases, 29 | useFactory: () => ({ 30 | getAllPostsByUser: jest.fn(() => true), 31 | getPostByUser: jest.fn(() => true), 32 | createPost: jest.fn(() => true), 33 | }), 34 | }, 35 | ], 36 | }).compile(); 37 | 38 | postsUseCases = module.get(PostsUseCases); 39 | postsController = module.get(PostsController); 40 | }); 41 | 42 | it('should return a list of PostVM when get a valid user with posts in GET /users/:userId/posts', async () => { 43 | jest 44 | .spyOn(postsUseCases, 'getAllPostsByUser') 45 | .mockImplementation(async () => [POST]); 46 | 47 | const postsVM = await postsController.getPostsByUser('1'); 48 | 49 | expect(postsVM).toHaveLength(1); 50 | expect(postsVM).toEqual([POST_VM]); 51 | }); 52 | 53 | it('should return the PostVM when get a valid posts of a valid user in GET /users/:userId/posts/:postId', async () => { 54 | jest 55 | .spyOn(postsUseCases, 'getPostByUser') 56 | .mockImplementation(async () => POST); 57 | 58 | const postVM = await postsController.getPost('1', '1'); 59 | 60 | expect(postVM instanceof PostVM).toBeTruthy(); 61 | expect(postVM).toEqual(POST_VM); 62 | }); 63 | 64 | it('should return a PostVM when creating a post in POST /users/:userId/posts', async () => { 65 | jest 66 | .spyOn(postsUseCases, 'createPost') 67 | .mockImplementation(async () => POST); 68 | 69 | const postVM = await postsController.createPost('1', POST); 70 | 71 | expect(postVM instanceof PostVM).toBeTruthy(); 72 | expect(postVM).toEqual(POST_VM); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/unit/presentation/UsersController.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { UsersController } from 'presentation/controllers/UsersController'; 3 | import { UsersUseCases } from 'application/use-cases/UsersUseCases'; 4 | import { User } from 'domain/models/User'; 5 | import { Post } from 'domain/models/Post'; 6 | import { UserVM } from 'presentation/view-models/users/UserVM'; 7 | 8 | describe('UsersController Test', () => { 9 | let usersController: UsersController; 10 | let usersUseCases: UsersUseCases; 11 | 12 | const POST = new Post('Title', 'Text', null, 1); 13 | POST.createdAt = new Date('2020-05-31 02:20:58.037572-03'); 14 | POST.updatedAt = new Date('2020-05-31 02:20:58.037572-03'); 15 | const USER = new User('John Doe', 'john.doe@gmail.com', [POST], 1); 16 | USER.createdAt = new Date('2020-05-31 02:20:58.037572-03'); 17 | USER.updatedAt = new Date('2020-05-31 02:20:58.037572-03'); 18 | 19 | const USER_VM = { 20 | id: 1, 21 | name: 'John Doe', 22 | email: 'john.doe@gmail.com', 23 | createdAt: new Date('2020-05-31 02:20:58.037572-03'), 24 | updatedAt: new Date('2020-05-31 02:20:58.037572-03'), 25 | } as UserVM; 26 | 27 | beforeEach(async () => { 28 | const module = await Test.createTestingModule({ 29 | providers: [ 30 | UsersController, 31 | { 32 | provide: UsersUseCases, 33 | useFactory: () => ({ 34 | getUserById: jest.fn(() => true), 35 | getUsers: jest.fn(() => true), 36 | createUser: jest.fn(() => true), 37 | }), 38 | }, 39 | ], 40 | }).compile(); 41 | 42 | usersUseCases = module.get(UsersUseCases); 43 | usersController = module.get(UsersController); 44 | }); 45 | 46 | it('should return the UserVM when get a valid user in GET /users/:id', async () => { 47 | jest 48 | .spyOn(usersUseCases, 'getUserById') 49 | .mockImplementation(async () => USER); 50 | 51 | const userVM = await usersController.get('1'); 52 | 53 | expect(userVM instanceof UserVM).toBeTruthy(); 54 | expect(userVM).toEqual(USER_VM); 55 | }); 56 | 57 | it('should return an list of UserVM when get all users in GET /users', async () => { 58 | jest 59 | .spyOn(usersUseCases, 'getUsers') 60 | .mockImplementation(async () => [USER]); 61 | 62 | const usersVM = await usersController.getAll(); 63 | 64 | expect(usersVM).toHaveLength(1); 65 | expect(usersVM).toEqual([USER_VM]); 66 | }); 67 | 68 | it('should return a UserVM when creating a user in POST /users', async () => { 69 | jest 70 | .spyOn(usersUseCases, 'createUser') 71 | .mockImplementation(async () => USER); 72 | 73 | const userVM = await usersController.createUser(USER); 74 | 75 | expect(userVM instanceof UserVM).toBeTruthy(); 76 | expect(userVM).toEqual(USER_VM); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /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 | "esModuleInterop": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./src", 13 | "incremental": true 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------