├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── development.env ├── dockerignore ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── application │ ├── application.module.ts │ ├── commands │ │ └── product.command.ts │ ├── createProduct.usecase.ts │ ├── deleteProduct.usecase.ts │ ├── factory │ │ └── product.factory.ts │ ├── getAllProducts.usecase.ts │ ├── getProduct.usecase.ts │ └── updateProduct.usecase.ts ├── domain │ ├── domain.module.ts │ ├── exceptions │ │ └── price-product-less-zero.exception.ts │ ├── ports │ │ └── product.repository.ts │ └── product.ts ├── infrastructure │ ├── adapters │ │ └── repository │ │ │ ├── entity │ │ │ └── product.entity.ts │ │ │ ├── product.repository.mongo.spec.ts │ │ │ ├── product.repository.mongo.ts │ │ │ └── schema │ │ │ └── product.schema.ts │ ├── config.module.ts │ ├── config.service.ts │ ├── controllers │ │ └── product.controller.ts │ ├── exceptions │ │ └── http-exception.filter.ts │ ├── infrastructure.module.ts │ └── mapper │ │ └── product.mapper.ts └── main.ts ├── test.env ├── test ├── jest-e2e.json └── src │ └── products │ ├── example.json │ └── products.e2e-spec.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb-base', 5 | 'plugin:import/typescript', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier', 8 | 'prettier/@typescript-eslint', 9 | ], 10 | settings: { 11 | "import/resolver": { 12 | node: { 13 | "paths": ["src"], 14 | extensions: [".js", ".jsx", ".ts", ".tsx"] 15 | } 16 | } 17 | }, 18 | root: true, 19 | env: { 20 | node: true, 21 | jest: true, 22 | }, 23 | rules: { 24 | 'import/no-unresolved': false, 25 | 'new-cap': 0, 26 | 'import/no-extraneous-dependencies': [ 27 | 'error', 28 | { devDependencies: ['**/*spec.ts'] }, 29 | ], 30 | 'no-useless-constructor': 'off', 31 | '@typescript-eslint/no-useless-constructor': 'error', 32 | "import/extensions": [ 33 | "error", 34 | "ignorePackages", 35 | { 36 | "js": "never", 37 | "jsx": "never", 38 | "ts": "never", 39 | "tsx": "never" 40 | } 41 | ] 42 | }, 43 | }; -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build --if-present 22 | - run: npm test 23 | env: 24 | CI: true 25 | - run: npm run test:e2e 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 AS builder 2 | WORKDIR /app 3 | COPY ./package.json ./ 4 | RUN npm install 5 | COPY . . 6 | RUN npm run build 7 | 8 | 9 | FROM node:10-alpine 10 | WORKDIR /app 11 | COPY --from=builder /app ./ 12 | CMD ["npm", "run", "start:prod"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Franklin Carrero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /development.env: -------------------------------------------------------------------------------- 1 | MONGO_SERVER_URL=localhost 2 | MONGO_SERVER_PORT=27017 3 | MONGO_SERVER_DBNAME=products -------------------------------------------------------------------------------- /dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-js-products-api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 16 | "test": "DEBUG=testcontainers jest --ci --verbose --detectOpenHandles", 17 | "test:watch": "DEBUG=testcontainers jest --watch", 18 | "test:cov": "DEBUG=testcontainers jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "DEBUG=testcontainers NODE_ENV=test jest --ci --verbose --detectOpenHandles --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^6.10.14", 24 | "@nestjs/core": "^6.10.14", 25 | "@nestjs/mongoose": "^6.3.1", 26 | "@nestjs/platform-express": "^6.10.14", 27 | "dotenv": "^8.2.0", 28 | "mongoose": "^5.8.10", 29 | "reflect-metadata": "^0.1.13", 30 | "rimraf": "^3.0.0", 31 | "rxjs": "^6.5.4", 32 | "typescript-optional": "^2.0.1" 33 | }, 34 | "devDependencies": { 35 | "@nestjs/cli": "^6.13.2", 36 | "@nestjs/schematics": "^6.8.1", 37 | "@nestjs/testing": "^6.10.14", 38 | "@types/express": "^4.17.2", 39 | "@types/jest": "^24.0.25", 40 | "@types/mongoose": "^5.5.43", 41 | "@types/node": "^13.1.6", 42 | "@types/supertest": "^2.0.8", 43 | "@typescript-eslint/eslint-plugin": "^2.12.0", 44 | "@typescript-eslint/parser": "^2.18.0", 45 | "eslint": "^5.16.0", 46 | "eslint-config-airbnb-base": "^14.0.0", 47 | "eslint-config-prettier": "^6.7.0", 48 | "eslint-plugin-import": "^2.20.0", 49 | "jest": "^24.9.0", 50 | "prettier": "^1.18.2", 51 | "supertest": "^4.0.2", 52 | "testcontainers": "^2.3.1", 53 | "ts-jest": "^24.3.0", 54 | "ts-loader": "^6.2.1", 55 | "ts-node": "^8.6.0", 56 | "tsconfig-paths": "^3.9.0", 57 | "typescript": "^3.7.4" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".spec.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule } from '@nestjs/common'; 2 | import DomainModule from './domain/domain.module'; 3 | import ApplicationModule from './application/application.module'; 4 | import InfrastructureModule from './infrastructure/infrastructure.module'; 5 | 6 | @Module({}) 7 | export default class AppModule { 8 | static foorRoot(setting: any): DynamicModule { 9 | return { 10 | module: AppModule, 11 | imports: [ 12 | DomainModule, 13 | ApplicationModule, 14 | InfrastructureModule.foorRoot(setting), 15 | ], 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/application/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import ProductRepositoryMongo from '../infrastructure/adapters/repository/product.repository.mongo'; 4 | import ProductSchema from '../infrastructure/adapters/repository/schema/product.schema'; 5 | import GetAllProductsUseCase from './getAllProducts.usecase'; 6 | import DomainModule from '../domain/domain.module'; 7 | import GetProductUseCase from './getProduct.usecase'; 8 | import CreateProductUseCase from './createProduct.usecase'; 9 | import DeleteProductUseCase from './deleteProduct.usecase'; 10 | import UpdateProductUseCase from './updateProduct.usecase'; 11 | import ProductFactory from './factory/product.factory'; 12 | 13 | @Module({ 14 | imports: [ 15 | DomainModule, 16 | MongooseModule.forFeature([ 17 | { 18 | name: 'Product', 19 | schema: ProductSchema, 20 | }, 21 | ]), 22 | ], 23 | providers: [ 24 | ProductFactory, 25 | GetAllProductsUseCase, 26 | GetProductUseCase, 27 | CreateProductUseCase, 28 | DeleteProductUseCase, 29 | UpdateProductUseCase, 30 | { 31 | provide: 'ProductRepository', 32 | useClass: ProductRepositoryMongo, 33 | }, 34 | ], 35 | exports: [ 36 | ProductFactory, 37 | GetAllProductsUseCase, 38 | GetProductUseCase, 39 | CreateProductUseCase, 40 | DeleteProductUseCase, 41 | UpdateProductUseCase, 42 | ], 43 | }) 44 | export default class ApplicationModule {} 45 | -------------------------------------------------------------------------------- /src/application/commands/product.command.ts: -------------------------------------------------------------------------------- 1 | export default class ProductCommand { 2 | public name: string; 3 | 4 | public description: string; 5 | 6 | public imageUrl: string; 7 | 8 | public price: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/application/createProduct.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, Logger } from '@nestjs/common'; 2 | import Product from 'src/domain/product'; 3 | import { ProductRepository } from 'src/domain/ports/product.repository'; 4 | import { Optional } from 'typescript-optional'; 5 | import ProductCommand from './commands/product.command'; 6 | import ProductFactory from './factory/product.factory'; 7 | 8 | @Injectable() 9 | export default class CreateProductUseCase { 10 | constructor( 11 | @Inject('ProductRepository') private productRepository: ProductRepository, 12 | private productFactory: ProductFactory, 13 | ) {} 14 | 15 | public handler(productCommand: ProductCommand): Promise> { 16 | const product = this.productFactory.createProduct(productCommand); 17 | return this.productRepository.createProduct(product); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/application/deleteProduct.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import Product from 'src/domain/product'; 3 | import { ProductRepository } from 'src/domain/ports/product.repository'; 4 | import { Optional } from 'typescript-optional'; 5 | 6 | @Injectable() 7 | export default class DeleteProductUseCase { 8 | constructor( 9 | @Inject('ProductRepository') private productRepository: ProductRepository, 10 | ) {} 11 | 12 | public handler(productId: string): Promise> { 13 | return this.productRepository.deleteProduct(productId); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/factory/product.factory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { Injectable } from '@nestjs/common'; 3 | import Product from '../../domain/product'; 4 | import ProductCommand from '../commands/product.command'; 5 | 6 | @Injectable() 7 | export default class ProductFactory { 8 | public createProduct(productCommand: ProductCommand): Product { 9 | return new Product( 10 | '', 11 | productCommand.name, 12 | productCommand.description, 13 | productCommand.imageUrl, 14 | productCommand.price, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/application/getAllProducts.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import Product from 'src/domain/product'; 3 | import { ProductRepository } from 'src/domain/ports/product.repository'; 4 | 5 | @Injectable() 6 | export default class GetAllProductsUseCase { 7 | constructor( 8 | @Inject('ProductRepository') private productRepository: ProductRepository, 9 | ) {} 10 | 11 | public handler(): Promise { 12 | return this.productRepository.getAll(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/application/getProduct.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import Product from 'src/domain/product'; 3 | import { ProductRepository } from 'src/domain/ports/product.repository'; 4 | import { Optional } from 'typescript-optional'; 5 | 6 | @Injectable() 7 | export default class GetProductUseCase { 8 | constructor( 9 | @Inject('ProductRepository') private productRepository: ProductRepository, 10 | ) {} 11 | 12 | public handler(productId: string): Promise> { 13 | return this.productRepository.getProduct(productId); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/updateProduct.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import Product from 'src/domain/product'; 3 | import { ProductRepository } from 'src/domain/ports/product.repository'; 4 | import { Optional } from 'typescript-optional'; 5 | 6 | @Injectable() 7 | export default class UpdateProductUseCase { 8 | constructor( 9 | @Inject('ProductRepository') private productRepository: ProductRepository, 10 | ) {} 11 | 12 | public handler( 13 | productId: string, 14 | product: Product, 15 | ): Promise> { 16 | return this.productRepository.updateProduct(productId, product); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/domain/domain.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({}) 4 | export default class DomainModule {} 5 | -------------------------------------------------------------------------------- /src/domain/exceptions/price-product-less-zero.exception.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-useless-constructor */ 2 | export default class PriceProductLessZeroException extends Error { 3 | constructor(message: string) { 4 | super(message); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/ports/product.repository.ts: -------------------------------------------------------------------------------- 1 | import Product from 'src/domain/product'; 2 | import { Optional } from 'typescript-optional'; 3 | 4 | export interface ProductRepository { 5 | getAll(): Promise; 6 | 7 | /** 8 | * Returns product filtered by id 9 | * @param {string} productId 10 | * @returns a `Product` object containing the data. 11 | */ 12 | getProduct(id: string): Promise>; 13 | 14 | createProduct(product: Product): Promise>; 15 | 16 | deleteProduct(productId: string): Promise>; 17 | 18 | updateProduct( 19 | productId: string, 20 | product: Product, 21 | ): Promise>; 22 | } 23 | -------------------------------------------------------------------------------- /src/domain/product.ts: -------------------------------------------------------------------------------- 1 | import PriceProductLessZeroException from './exceptions/price-product-less-zero.exception'; 2 | 3 | export default class Product { 4 | private id?: string; 5 | 6 | private readonly name: string; 7 | 8 | private readonly description: string; 9 | 10 | private readonly imageUrl: string; 11 | 12 | private readonly price: number; 13 | 14 | private createAt: Date; 15 | 16 | constructor( 17 | id: string, 18 | name: string, 19 | description: string, 20 | imageUrl: string, 21 | price: number, 22 | ) { 23 | this.id = id; 24 | this.name = name; 25 | this.description = description; 26 | this.imageUrl = imageUrl; 27 | this.price = price || 0; 28 | this.validatePrice(); 29 | } 30 | 31 | public validatePrice(): void { 32 | if (this.price <= 0) { 33 | throw new PriceProductLessZeroException( 34 | 'The price product should be greater than zero', 35 | ); 36 | } 37 | } 38 | 39 | public getName(): string { 40 | return this.name; 41 | } 42 | 43 | public getId(): string { 44 | return this.id; 45 | } 46 | 47 | public setCreateAt(createdAt: Date): this { 48 | this.createAt = createdAt; 49 | return this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/infrastructure/adapters/repository/entity/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface ProductEntity extends Document { 4 | name: string; 5 | readonly description: string; 6 | readonly imageUrl: string; 7 | readonly price: number; 8 | readonly createAt: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/infrastructure/adapters/repository/product.repository.mongo.spec.ts: -------------------------------------------------------------------------------- 1 | import { GenericContainer, Wait } from 'testcontainers'; 2 | import { TestingModule, Test } from '@nestjs/testing'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import InfrastructureModule from '../../infrastructure.module'; 5 | 6 | import ProductRepositoryMongo from './product.repository.mongo'; 7 | import ProductSchema from './schema/product.schema'; 8 | import Product from '../../../domain/product'; 9 | 10 | describe('productRepositoryMongo', () => { 11 | let productRepositoryMongo: ProductRepositoryMongo; 12 | let container; 13 | const mongoPort = 27017; 14 | jest.setTimeout(30000); 15 | beforeAll(async done => { 16 | container = await new GenericContainer('mongo') 17 | .withExposedPorts(mongoPort) 18 | .withWaitStrategy(Wait.forLogMessage('Listening on 0.0.0.0')) 19 | .start(); 20 | 21 | const setting = { port: container.getMappedPort(mongoPort) }; 22 | const module: TestingModule = await Test.createTestingModule({ 23 | imports: [ 24 | InfrastructureModule.foorRoot(setting), 25 | MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]), 26 | ], 27 | providers: [ProductRepositoryMongo], 28 | }).compile(); 29 | 30 | productRepositoryMongo = module.get( 31 | ProductRepositoryMongo, 32 | ); 33 | 34 | done(); 35 | }); 36 | 37 | afterAll(async done => { 38 | container.stop(); 39 | done(); 40 | }); 41 | 42 | afterEach(async done => { 43 | await container.exec([ 44 | 'mongo', 45 | 'products', 46 | '--eval', 47 | "'db.dropDatabase();'", 48 | ]); 49 | done(); 50 | }); 51 | 52 | describe('getAll products', () => { 53 | it('when get all products', async done => { 54 | const product = new Product( 55 | '', 56 | 'consola', 57 | 'prueba', 58 | 'https://upload.wikimedia.org/wikipedia/commons/5/5d/Sony-PSP-1000-Body.png', 59 | 500, 60 | ); 61 | product.setCreateAt(new Date(2017, 2, 7)); 62 | await productRepositoryMongo.createProduct(product); 63 | 64 | const products = await productRepositoryMongo.getAll(); 65 | 66 | expect(products.length).toBe(1); 67 | expect(products[0].getName()).toBe('consola'); 68 | done(); 69 | }); 70 | }); 71 | 72 | describe('get product', () => { 73 | it('when get product with id', async done => { 74 | const product = new Product( 75 | '354564456456', 76 | 'x-box-one', 77 | 'prueba', 78 | 'https://upload.wikimedia.org/wikipedia/commons/5/5d/Sony-PSP-1000-Body.png', 79 | 500, 80 | ); 81 | product.setCreateAt(new Date(2017, 2, 7)); 82 | const productSaved = await productRepositoryMongo.createProduct(product); 83 | 84 | const ProductExpect = await productRepositoryMongo.getProduct( 85 | productSaved.get().getId(), 86 | ); 87 | expect(ProductExpect.isPresent()).toBeTruthy(); 88 | expect(ProductExpect.get().getId()).toEqual(productSaved.get().getId()); 89 | expect(ProductExpect.get().getName()).toBe('x-box-one'); 90 | done(); 91 | }); 92 | }); 93 | 94 | describe('create product', () => { 95 | it('when create product then return product', async done => { 96 | const product = new Product( 97 | '354564456456', 98 | 'macbook pro', 99 | 'prueba de guardado', 100 | 'https://upload.wikimedia.org/wikipedia/commons/5/5d/Sony-PSP-1000-Body.png', 101 | 500, 102 | ); 103 | product.setCreateAt(new Date(2017, 2, 7)); 104 | const productSaved = await productRepositoryMongo.createProduct(product); 105 | 106 | expect(productSaved.isPresent()).toBeTruthy(); 107 | expect(productSaved.get().getId()).not.toBeUndefined(); 108 | expect(productSaved.get().getName()).toBe('macbook pro'); 109 | done(); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/infrastructure/adapters/repository/product.repository.mongo.ts: -------------------------------------------------------------------------------- 1 | import { InjectModel } from '@nestjs/mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { Model } from 'mongoose'; 4 | import Product from 'src/domain/product'; 5 | import { ProductEntity } from 'src/infrastructure/adapters/repository/entity/product.entity'; 6 | import { Optional } from 'typescript-optional'; 7 | import ProductMapper from '../../mapper/product.mapper'; 8 | import { ProductRepository } from '../../../domain/ports/product.repository'; 9 | 10 | @Injectable() 11 | export default class ProductRepositoryMongo implements ProductRepository { 12 | constructor( 13 | @InjectModel('Product') private readonly productModel: Model, 14 | ) {} 15 | 16 | public async getAll(): Promise { 17 | const products = await this.productModel.find(); 18 | return ProductMapper.toDomains(products); 19 | } 20 | 21 | public async createProduct(product: Product): Promise> { 22 | let productCreated = new this.productModel(product); 23 | productCreated = await productCreated.save(); 24 | return ProductMapper.toDomain(productCreated); 25 | } 26 | 27 | public async getProduct(productId: string): Promise> { 28 | const product = await this.productModel.findById(productId); 29 | return ProductMapper.toDomain(product); 30 | } 31 | 32 | public async deleteProduct(productId: string): Promise> { 33 | const productDeleted = await this.productModel.findByIdAndDelete(productId); 34 | return ProductMapper.toDomain(productDeleted); 35 | } 36 | 37 | public async updateProduct( 38 | productId: string, 39 | product: Product, 40 | ): Promise> { 41 | const productUpdated = await this.productModel.findByIdAndUpdate( 42 | productId, 43 | product, 44 | { new: true }, 45 | ); 46 | return ProductMapper.toDomain(productUpdated); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/infrastructure/adapters/repository/schema/product.schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | 3 | const ProductSchema = new Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | }, 8 | description: String, 9 | imageUrl: String, 10 | price: Number, 11 | createAt: { 12 | type: Date, 13 | default: Date.now, 14 | }, 15 | }); 16 | 17 | export default ProductSchema; 18 | -------------------------------------------------------------------------------- /src/infrastructure/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService( 9 | `${process.env.NODE_ENV || 'development'}.env`, 10 | ), 11 | }, 12 | ], 13 | exports: [ConfigService], 14 | }) 15 | export class ConfigModule {} 16 | -------------------------------------------------------------------------------- /src/infrastructure/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as fs from 'fs'; 3 | 4 | export class ConfigService { 5 | private readonly envConfig: { [key: string]: string }; 6 | 7 | constructor(filePath: string) { 8 | // stock the file 9 | this.envConfig = dotenv.parse(fs.readFileSync(filePath)); 10 | } 11 | 12 | // get specific key in .env file 13 | get(key: string): string { 14 | return this.envConfig[key]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Put, 6 | Delete, 7 | Res, 8 | HttpStatus, 9 | Body, 10 | Param, 11 | } from '@nestjs/common'; 12 | import ProductCommand from '../../application/commands/product.command'; 13 | import GetAllProductsUseCase from '../../application/getAllProducts.usecase'; 14 | import GetProductUseCase from '../../application/getProduct.usecase'; 15 | import Product from '../../domain/product'; 16 | import CreateProductUseCase from '../../application/createProduct.usecase'; 17 | import DeleteProductUseCase from '../../application/deleteProduct.usecase'; 18 | import UpdateProductUseCase from '../../application/updateProduct.usecase'; 19 | 20 | @Controller('products/') 21 | export default class ProductController { 22 | constructor( 23 | private getAllProductsUseCase: GetAllProductsUseCase, 24 | private readonly getProductUseCase: GetProductUseCase, 25 | private readonly createProductUseCase: CreateProductUseCase, 26 | private readonly deleteProductUseCase: DeleteProductUseCase, 27 | private readonly updateProductUseCase: UpdateProductUseCase, 28 | ) {} 29 | 30 | @Get() 31 | public async getProducts(@Res() request): Promise { 32 | const products = await this.getAllProductsUseCase.handler(); 33 | return request.status(HttpStatus.OK).json(products); 34 | } 35 | 36 | @Get(':id') 37 | public async getProduct( 38 | @Res() request, 39 | @Param('id') id: string, 40 | ): Promise { 41 | const product = await this.getProductUseCase.handler(id); 42 | return request.status(HttpStatus.OK).json(product); 43 | } 44 | 45 | @Post() 46 | public async createProduct( 47 | @Res() request, 48 | @Body() product: ProductCommand, 49 | ): Promise { 50 | const productCreated = await this.createProductUseCase.handler(product); 51 | return request.status(HttpStatus.CREATED).json(productCreated); 52 | } 53 | 54 | @Delete(':id') 55 | public async deleteProduct( 56 | @Res() request, 57 | @Param('id') id: string, 58 | ): Promise { 59 | const product = await this.deleteProductUseCase.handler(id); 60 | return request.status(HttpStatus.OK).json(product); 61 | } 62 | 63 | @Put(':id') 64 | public async updateProduct( 65 | @Res() request, 66 | @Param('id') id: string, 67 | @Body() product: Product, 68 | ): Promise { 69 | const productUpdated = await this.updateProductUseCase.handler(id, product); 70 | return request.status(HttpStatus.OK).json(productUpdated); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/infrastructure/exceptions/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /* eslint-disable class-methods-use-this */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | import { ExceptionFilter, Catch, ArgumentsHost, Logger } from '@nestjs/common'; 5 | import { Request, Response } from 'express'; 6 | import PriceProductLessZeroException from 'src/domain/exceptions/price-product-less-zero.exception'; 7 | 8 | @Catch() 9 | export default class HttpExceptionFilter implements ExceptionFilter { 10 | catch(exception: Error, host: ArgumentsHost) { 11 | const ctx = host.switchToHttp(); 12 | const response = ctx.getResponse(); 13 | const request = ctx.getRequest(); 14 | 15 | const { message, status } = this.isBusinessException(exception); 16 | response.status(status).json({ 17 | message, 18 | statusCode: status, 19 | timestamp: new Date().toISOString(), 20 | path: request.url, 21 | }); 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | public isBusinessException(exception: Error): any { 26 | if (exception instanceof PriceProductLessZeroException) { 27 | return { 28 | message: exception.message, 29 | status: 400, 30 | }; 31 | } 32 | Logger.log(exception.stack); 33 | return { 34 | message: 'unknown', 35 | status: 500, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/infrastructure/infrastructure.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import ApplicationModule from '../application/application.module'; 4 | import ProductSchema from './adapters/repository/schema/product.schema'; 5 | import ProductController from './controllers/product.controller'; 6 | import { ConfigModule } from './config.module'; 7 | import { ConfigService } from './config.service'; 8 | 9 | const db_uri = 'MONGO_SERVER_URL'; 10 | const db_port = 'MONGO_SERVER_PORT'; 11 | const db_name = 'MONGO_SERVER_DBNAME'; 12 | 13 | @Module({}) 14 | export default class InfrastructureModule { 15 | static foorRoot(setting: any): DynamicModule { 16 | return { 17 | module: InfrastructureModule, 18 | imports: [ 19 | ApplicationModule, 20 | MongooseModule.forRootAsync({ 21 | imports: [ConfigModule], 22 | useFactory: async (configService: ConfigService) => ({ 23 | uri: `mongodb://${configService.get(db_uri)}:${setting.port || 24 | configService.get(db_port)}/${configService.get(db_name)}`, 25 | }), 26 | inject: [ConfigService], 27 | }), 28 | MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]), 29 | ], 30 | controllers: [ProductController], 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infrastructure/mapper/product.mapper.ts: -------------------------------------------------------------------------------- 1 | import { Optional } from 'typescript-optional'; 2 | import Product from '../../domain/product'; 3 | import { ProductEntity } from '../adapters/repository/entity/product.entity'; 4 | 5 | export default class ProductMapper { 6 | public static toDomain(productEntity: ProductEntity): Optional { 7 | if (!productEntity) { 8 | return Optional.empty(); 9 | } 10 | const product = new Product( 11 | productEntity.id, 12 | productEntity.name, 13 | productEntity.description, 14 | productEntity.imageUrl, 15 | productEntity.price, 16 | ); 17 | 18 | product.setCreateAt(new Date(productEntity.createAt)); 19 | return Optional.of(product); 20 | } 21 | 22 | public static toDomains(productsEntity: ProductEntity[]): Product[] { 23 | const products = new Array(); 24 | productsEntity.forEach(productEntity => { 25 | const product = this.toDomain(productEntity); 26 | products.push(product.get()); 27 | }); 28 | return products; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import AppModule from './app.module'; 3 | import HttpExceptionFilter from './infrastructure/exceptions/http-exception.filter'; 4 | 5 | async function bootstrap(): Promise { 6 | const app = await NestFactory.create(AppModule.foorRoot({})); 7 | app.useGlobalFilters(new HttpExceptionFilter()); 8 | await app.listen(3000); 9 | } 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /test.env: -------------------------------------------------------------------------------- 1 | MONGO_SERVER_URL=localhost 2 | MONGO_SERVER_PORT=27018 3 | MONGO_SERVER_DBNAME=products -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "src", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/src/products/example.json: -------------------------------------------------------------------------------- 1 | {"name" : "poratil","description" : "new portatil","imageUrl" : "https://co-media.hptiendaenlinea.com/catalog/product/cache/b3b166914d87ce343d4dc5ec5117b502/4/P/4PF98LA-1_T1562703002.png","price" : 1000} -------------------------------------------------------------------------------- /test/src/products/products.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { GenericContainer, Wait } from 'testcontainers'; 5 | import AppModule from '../../../src/app.module'; 6 | 7 | const fs = require('fs'); 8 | 9 | describe('ProductsController (e2e)', () => { 10 | let app: INestApplication; 11 | let container; 12 | const portMongo = 27017; 13 | jest.setTimeout(30000); 14 | 15 | beforeAll(async done => { 16 | container = await new GenericContainer('mongo') 17 | .withExposedPorts(portMongo) 18 | .withWaitStrategy(Wait.forLogMessage('Listening on 0.0.0.0')) 19 | .start(); 20 | done(); 21 | }); 22 | 23 | afterAll(async done => { 24 | container.stop(); 25 | done(); 26 | }); 27 | 28 | beforeEach(async done => { 29 | const moduleFixture: TestingModule = await Test.createTestingModule({ 30 | imports: [ 31 | AppModule.foorRoot({ port: container.getMappedPort(portMongo) }), 32 | ], 33 | }).compile(); 34 | 35 | app = moduleFixture.createNestApplication(); 36 | await app.init(); 37 | done(); 38 | }); 39 | 40 | it('/ (Post)', async done => { 41 | const rawdata = fs.readFileSync(`${__dirname}/example.json`); 42 | 43 | const response = await request(app.getHttpServer()) 44 | .post('/products/') 45 | .send(JSON.parse(rawdata)); 46 | 47 | expect(response.status).toBe(201); 48 | expect(response.body.id).not.toBeNull(); 49 | expect(response.body.price).toBe(1000); 50 | done(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /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 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------