├── .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 |
3 |
4 |
5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
6 | [travis-url]: https://travis-ci.org/nestjs/nest
7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
8 | [linux-url]: https://travis-ci.org/nestjs/nest
9 |
10 | A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 | ## Description
28 |
29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
30 |
31 | ## Installation
32 |
33 | ```bash
34 | $ npm install
35 | ```
36 |
37 | ## Running the app
38 |
39 | ```bash
40 | # development
41 | $ npm run start
42 |
43 | # watch mode
44 | $ npm run start:dev
45 |
46 | # production mode
47 | $ npm run start:prod
48 | ```
49 |
50 | ## Test
51 |
52 | ```bash
53 | # unit tests
54 | $ npm run test
55 |
56 | # e2e tests
57 | $ npm run test:e2e
58 |
59 | # test coverage
60 | $ npm run test:cov
61 | ```
62 |
63 | ## Support
64 |
65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
66 |
67 | ## Stay in touch
68 |
69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
70 | - Website - [https://nestjs.com](https://nestjs.com/)
71 | - Twitter - [@nestframework](https://twitter.com/nestframework)
72 |
73 | ## License
74 |
75 | Nest is [MIT licensed](LICENSE).
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------