├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── README.md ├── __test__ ├── order.service.test.ts └── product.service.test.ts ├── build ├── routes.ts └── swagger.json ├── jest.config.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── prisma ├── dev.db ├── index.ts ├── schema.prisma └── singleton.jest.ts ├── src ├── app.ts ├── order-management │ ├── application │ │ └── order.service.ts │ ├── domain │ │ ├── interfaces │ │ │ └── order.interface.ts │ │ └── order.ts │ └── infraestructure │ │ ├── controllers │ │ └── order.controller.ts │ │ └── order.repository.ts ├── product-management │ ├── application │ │ └── product.service.ts │ ├── domain │ │ ├── interfaces │ │ │ └── product.interface.ts │ │ └── product.ts │ └── infraestructure │ │ ├── controllers │ │ └── product.controller.ts │ │ └── product.repository.ts └── server.ts ├── tsconfig.json └── tsoa.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT = 3001 2 | 3 | # sqlite path 4 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: 'standard-with-typescript', 7 | overrides: [ 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module' 12 | }, 13 | rules: { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :tv: Este proyecto fue generado en el directo de mi canal de Twitch [Zatge](https://www.twitch.tv/zatge) 2 | 3 | :wrench: **Asegurate** de tener instalados [Npm, Node](https://nodejs.org/es), [ts-node](https://www.npmjs.com/package/ts-node) y [Typescript](https://www.npmjs.com/package/typescript) de demanera global. 4 | 5 | :memo: **Recuerda** quitar el `.example` al archivo `.env.example` y configurar las variables de entorno. 6 | 7 | :heavy_check_mark: Paso a paso 8 | ```bash 9 | npm install && \ 10 | npm run build && \ 11 | npm run dev 12 | ``` 13 | 14 | :hammer: ¿Como contribuir? 15 | 16 | Explora el proyecto :mag_right:, familiarizate con la estructura, las librerias que usamos, intenta levantar el proyecto de manera local. Busca en la lista de issues el que mas te interese y que puedas resolver, asegurate de leer la descripción y los comentarios para entender mejor el problema. 17 | 18 | En el apartado de Issues veras generados algunos tickets donde cualquier podra trabajar en alguno de ellos en su repositorio 19 | 20 | Forkea el repo :twisted_rightwards_arrows:, Esto creara una copia del repo en tu cuenta de Github. 21 | Clona ese repo en tu entorno local utilizando `git clone "url de tu repo"` 22 | Crea una nueva rama :arrow_heading_down: con el nombre del issue y ID unico de la issue (Es el numero que aparece en la url cuando estas en una issue) 23 | Haz todos los cambios que consideres necesario, Genera un pull request con una descripción explicando la solución connotando todo lo que consideres importante para el validador de la PR. 24 | 25 | > :warning: Es importante que cada porción del codigo este commiteado de forma modular, si solo hay un commit con todos los cambios realizados de un solo tiron invalidare la pull request. 26 | 27 | > :memo: La rama deberia tener una estructura similar a `IDISSUE/feature/nombre-del-issue` (Respetar los guiones cuando hay espacios) 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /__test__/order.service.test.ts: -------------------------------------------------------------------------------- 1 | import { OrderService } from '../src/order-management/application/order.service'; 2 | import { IOrderRepository } from '../src/order-management/domain/interfaces/order.interface'; 3 | import { Order } from '../src/order-management/domain/order'; 4 | class MockOrderRepository implements IOrderRepository { 5 | private order: Order[] = []; 6 | 7 | async addOrder(order: Order) { 8 | this.order.push(order); 9 | return order; 10 | } 11 | } 12 | 13 | describe('OrderService', () => { 14 | let orderService: OrderService; 15 | 16 | beforeEach(() => { 17 | var mockOrderRepository = new MockOrderRepository(); 18 | orderService = new OrderService(mockOrderRepository); 19 | }); 20 | 21 | test("addOrder adds a order to the database", async () => { 22 | const order: Order = new Order(1, 2); 23 | const addedOrder = await orderService.addOrder(1, 2); 24 | 25 | expect(addedOrder.productId).toBe(order.productId); 26 | expect(addedOrder.total).toBe(order.total); 27 | }) 28 | }) -------------------------------------------------------------------------------- /__test__/product.service.test.ts: -------------------------------------------------------------------------------- 1 | import { IProductRepository } from '../src/product-management/domain/interfaces/product.interface'; 2 | 3 | import { ProductService } from 'src/product-management/application/product.service'; 4 | import { Product } from 'src/product-management/domain/product'; 5 | 6 | class MockProductRepository implements IProductRepository { 7 | private products: Product[] = [] 8 | addProduct(product: Product): Promise { 9 | this.products.push(product); 10 | return Promise.resolve(product); 11 | } 12 | } 13 | 14 | describe('ProductService', () => { 15 | let productService: ProductService; 16 | 17 | beforeEach(() => { 18 | const mockProductRepository = new MockProductRepository(); 19 | productService = new ProductService(mockProductRepository); 20 | }); 21 | 22 | test("addProducts adds a product to the database", async () => { 23 | const product: Product = new Product("test", 100); 24 | const addedProduct = await productService.addProduct(product.name, product.price); 25 | 26 | expect(addedProduct.name).toBe(product.productId); 27 | expect(addedProduct.price).toBe(product.price); 28 | }) 29 | }) -------------------------------------------------------------------------------- /build/routes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 4 | import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime'; 5 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 6 | import { OrderController } from '../src/order-management/infraestructure/controllers/order.controller'; 7 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 8 | import { ProductController } from '../src/product-management/infraestructure/controllers/product.controller'; 9 | import type { RequestHandler, Router } from 'express'; 10 | 11 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 12 | 13 | const models: TsoaRoute.Models = { 14 | "Order": { 15 | "dataType": "refObject", 16 | "properties": { 17 | "orderId": {"dataType":"double","required":true}, 18 | "total": {"dataType":"double","required":true}, 19 | "productId": {"dataType":"double","required":true}, 20 | "createdAt": {"dataType":"datetime","required":true}, 21 | }, 22 | "additionalProperties": false, 23 | }, 24 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 25 | "Product": { 26 | "dataType": "refObject", 27 | "properties": { 28 | "productId": {"dataType":"double","required":true}, 29 | "name": {"dataType":"string","required":true}, 30 | "price": {"dataType":"double","required":true}, 31 | }, 32 | "additionalProperties": false, 33 | }, 34 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 35 | }; 36 | const validationService = new ValidationService(models); 37 | 38 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 39 | 40 | export function RegisterRoutes(app: Router) { 41 | // ########################################################################################################### 42 | // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look 43 | // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa 44 | // ########################################################################################################### 45 | app.post('/order', 46 | ...(fetchMiddlewares(OrderController)), 47 | ...(fetchMiddlewares(OrderController.prototype.addOrder)), 48 | 49 | function OrderController_addOrder(request: any, response: any, next: any) { 50 | const args = { 51 | requestBody: {"in":"body","name":"requestBody","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"productId":{"dataType":"double","required":true},"total":{"dataType":"double","required":true}}}, 52 | }; 53 | 54 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 55 | 56 | let validatedArgs: any[] = []; 57 | try { 58 | validatedArgs = getValidatedArgs(args, request, response); 59 | 60 | const controller = new OrderController(); 61 | 62 | 63 | const promise = controller.addOrder.apply(controller, validatedArgs as any); 64 | promiseHandler(controller, promise, response, undefined, next); 65 | } catch (err) { 66 | return next(err); 67 | } 68 | }); 69 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 70 | app.post('/products', 71 | ...(fetchMiddlewares(ProductController)), 72 | ...(fetchMiddlewares(ProductController.prototype.addProduct)), 73 | 74 | function ProductController_addProduct(request: any, response: any, next: any) { 75 | const args = { 76 | requestBody: {"in":"body","name":"requestBody","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"price":{"dataType":"double","required":true},"name":{"dataType":"string","required":true}}}, 77 | }; 78 | 79 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 80 | 81 | let validatedArgs: any[] = []; 82 | try { 83 | validatedArgs = getValidatedArgs(args, request, response); 84 | 85 | const controller = new ProductController(); 86 | 87 | 88 | const promise = controller.addProduct.apply(controller, validatedArgs as any); 89 | promiseHandler(controller, promise, response, undefined, next); 90 | } catch (err) { 91 | return next(err); 92 | } 93 | }); 94 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 95 | 96 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 97 | 98 | 99 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 100 | 101 | function isController(object: any): object is Controller { 102 | return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object; 103 | } 104 | 105 | function promiseHandler(controllerObj: any, promise: any, response: any, successStatus: any, next: any) { 106 | return Promise.resolve(promise) 107 | .then((data: any) => { 108 | let statusCode = successStatus; 109 | let headers; 110 | if (isController(controllerObj)) { 111 | headers = controllerObj.getHeaders(); 112 | statusCode = controllerObj.getStatus() || statusCode; 113 | } 114 | 115 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 116 | 117 | returnHandler(response, statusCode, data, headers) 118 | }) 119 | .catch((error: any) => next(error)); 120 | } 121 | 122 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 123 | 124 | function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) { 125 | if (response.headersSent) { 126 | return; 127 | } 128 | Object.keys(headers).forEach((name: string) => { 129 | response.set(name, headers[name]); 130 | }); 131 | if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { 132 | response.status(statusCode || 200) 133 | data.pipe(response); 134 | } else if (data !== null && data !== undefined) { 135 | response.status(statusCode || 200).json(data); 136 | } else { 137 | response.status(statusCode || 204).end(); 138 | } 139 | } 140 | 141 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 142 | 143 | function responder(response: any): TsoaResponse { 144 | return function(status, data, headers) { 145 | returnHandler(response, status, data, headers); 146 | }; 147 | }; 148 | 149 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 150 | 151 | function getValidatedArgs(args: any, request: any, response: any): any[] { 152 | const fieldErrors: FieldErrors = {}; 153 | const values = Object.keys(args).map((key) => { 154 | const name = args[key].name; 155 | switch (args[key].in) { 156 | case 'request': 157 | return request; 158 | case 'query': 159 | return validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 160 | case 'queries': 161 | return validationService.ValidateParam(args[key], request.query, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 162 | case 'path': 163 | return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 164 | case 'header': 165 | return validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 166 | case 'body': 167 | return validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 168 | case 'body-prop': 169 | return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', {"noImplicitAdditionalProperties":"throw-on-extras"}); 170 | case 'formData': 171 | if (args[key].dataType === 'file') { 172 | return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 173 | } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { 174 | return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 175 | } else { 176 | return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 177 | } 178 | case 'res': 179 | return responder(response); 180 | } 181 | }); 182 | 183 | if (Object.keys(fieldErrors).length > 0) { 184 | throw new ValidateError(fieldErrors, ''); 185 | } 186 | return values; 187 | } 188 | 189 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 190 | } 191 | 192 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 193 | -------------------------------------------------------------------------------- /build/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "examples": {}, 4 | "headers": {}, 5 | "parameters": {}, 6 | "requestBodies": {}, 7 | "responses": {}, 8 | "schemas": { 9 | "Order": { 10 | "properties": { 11 | "orderId": { 12 | "type": "number", 13 | "format": "double" 14 | }, 15 | "total": { 16 | "type": "number", 17 | "format": "double" 18 | }, 19 | "productId": { 20 | "type": "number", 21 | "format": "double" 22 | }, 23 | "createdAt": { 24 | "type": "string", 25 | "format": "date-time" 26 | } 27 | }, 28 | "required": [ 29 | "orderId", 30 | "total", 31 | "productId", 32 | "createdAt" 33 | ], 34 | "type": "object", 35 | "additionalProperties": false 36 | }, 37 | "Product": { 38 | "properties": { 39 | "productId": { 40 | "type": "number", 41 | "format": "double" 42 | }, 43 | "name": { 44 | "type": "string" 45 | }, 46 | "price": { 47 | "type": "number", 48 | "format": "double" 49 | } 50 | }, 51 | "required": [ 52 | "productId", 53 | "name", 54 | "price" 55 | ], 56 | "type": "object", 57 | "additionalProperties": false 58 | } 59 | }, 60 | "securitySchemes": {} 61 | }, 62 | "info": { 63 | "title": "hexagon-architecture", 64 | "version": "1.0.0", 65 | "license": { 66 | "name": "ISC" 67 | }, 68 | "contact": {} 69 | }, 70 | "openapi": "3.0.0", 71 | "paths": { 72 | "/order": { 73 | "post": { 74 | "operationId": "AddOrder", 75 | "responses": { 76 | "200": { 77 | "description": "Ok", 78 | "content": { 79 | "application/json": { 80 | "schema": { 81 | "$ref": "#/components/schemas/Order" 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | "security": [], 88 | "parameters": [], 89 | "requestBody": { 90 | "required": true, 91 | "content": { 92 | "application/json": { 93 | "schema": { 94 | "properties": { 95 | "productId": { 96 | "type": "number", 97 | "format": "double" 98 | }, 99 | "total": { 100 | "type": "number", 101 | "format": "double" 102 | } 103 | }, 104 | "required": [ 105 | "productId", 106 | "total" 107 | ], 108 | "type": "object" 109 | } 110 | } 111 | } 112 | } 113 | } 114 | }, 115 | "/products": { 116 | "post": { 117 | "operationId": "AddProduct", 118 | "responses": { 119 | "200": { 120 | "description": "Ok", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Product" 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "security": [], 131 | "parameters": [], 132 | "requestBody": { 133 | "required": true, 134 | "content": { 135 | "application/json": { 136 | "schema": { 137 | "properties": { 138 | "price": { 139 | "type": "number", 140 | "format": "double" 141 | }, 142 | "name": { 143 | "type": "string" 144 | } 145 | }, 146 | "required": [ 147 | "price", 148 | "name" 149 | ], 150 | "type": "object" 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | }, 158 | "servers": [ 159 | { 160 | "url": "/" 161 | } 162 | ] 163 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type {Config} from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testMatch: ['**/__test__/**/*.test.ts'], 6 | }; 7 | 8 | export default config; -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "exec": "ts-node src/server.ts", 7 | "ignore": [ 8 | "src/**/*.spec.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexagon-architecture", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "build/src/server.js", 6 | "scripts": { 7 | "dev": "concurrently \"nodemon\" \"tsoa spec-and-routes\"", 8 | "build": "npm run db:generate && tsoa spec-and-routes && tsc", 9 | "start": "node build/src/server.js", 10 | "test": "jest", 11 | "db:generate": "npx prisma generate", 12 | "db:migrate": "npx prisma migrate dev", 13 | "db:deploy": "npx prisma migrate deploy", 14 | "db:push": "npx prisma db push", 15 | "db:seed": "ts-node src/seed/seed.ts" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@types/express": "^4.17.17", 22 | "@types/jest": "^29.5.0", 23 | "@types/node": "^18.15.11", 24 | "@types/swagger-ui-express": "^4.1.3", 25 | "@typescript-eslint/eslint-plugin": "^5.58.0", 26 | "concurrently": "^8.0.1", 27 | "eslint": "^8.38.0", 28 | "eslint-config-standard-with-typescript": "^34.0.1", 29 | "eslint-plugin": "^1.0.1", 30 | "eslint-plugin-import": "^2.27.5", 31 | "eslint-plugin-local-rules": "^1.3.2", 32 | "eslint-plugin-n": "^15.7.0", 33 | "eslint-plugin-promise": "^6.1.1", 34 | "eslint-plugin-regex": "^1.10.0", 35 | "eslint-plugin-regexp": "^1.14.0", 36 | "jest": "^29.5.0", 37 | "jest-mock-extended": "^3.0.4", 38 | "nodemon": "^2.0.22", 39 | "ts-jest": "^29.1.0", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^5.0.4" 42 | }, 43 | "dependencies": { 44 | "@prisma/client": "^4.12.0", 45 | "express": "^4.18.2", 46 | "prisma": "^4.12.0", 47 | "swagger-ui-express": "^4.6.2", 48 | "tsoa": "^5.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperMartin/architecture-hexagonal-node/e25faa9d6d1e01bff1138c010f144f86e43dd146/prisma/dev.db -------------------------------------------------------------------------------- /prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | export * from '@prisma/client' 3 | const prisma = new PrismaClient() 4 | export default prisma 5 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Product { 14 | productId Int @id @default(autoincrement()) 15 | name String 16 | price Float 17 | 18 | orders Order[] 19 | 20 | @@map("products") 21 | } 22 | 23 | model Order { 24 | orderId Int @id @default(autoincrement()) 25 | total Float 26 | productId Int 27 | createdAt DateTime @default(now()) 28 | 29 | products Product @relation(references: [productId], fields: [productId]) 30 | 31 | @@map("orders") 32 | } 33 | -------------------------------------------------------------------------------- /prisma/singleton.jest.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended' 3 | 4 | import prisma from './index' 5 | 6 | jest.mock('./index', () => ({ 7 | __esModule: true, 8 | default: mockDeep(), 9 | })) 10 | 11 | beforeEach(() => { 12 | mockReset(prismaMock) 13 | }) 14 | 15 | export const prismaMock = prisma as unknown as DeepMockProxy -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | // src/app.ts 2 | import express, { Response as ExResponse, Request as ExRequest, urlencoded, json } from "express"; 3 | import { RegisterRoutes } from "../build/routes"; 4 | import swaggerUi from "swagger-ui-express"; 5 | 6 | export const app = express(); 7 | 8 | app.use("/docs", swaggerUi.serve, async (_req: ExRequest, res: ExResponse) => { 9 | return res.send( 10 | swaggerUi.generateHTML(await import("../build/swagger.json")) 11 | ); 12 | }); 13 | 14 | // Use body parser to read sent json payloads 15 | app.use( 16 | urlencoded({ 17 | extended: true, 18 | }) 19 | ); 20 | app.use(json()); 21 | 22 | RegisterRoutes(app); 23 | -------------------------------------------------------------------------------- /src/order-management/application/order.service.ts: -------------------------------------------------------------------------------- 1 | import { IOrderRepository } from '../domain/interfaces/order.interface'; 2 | import { Order } from '../domain/order'; 3 | export class OrderService { 4 | private orderRepository: IOrderRepository; 5 | 6 | constructor(orderRepository: IOrderRepository){ 7 | this.orderRepository = orderRepository; 8 | } 9 | 10 | public async addOrder(productId:number, total: number): Promise { 11 | const order: Order = new Order(productId, total); 12 | return await this.orderRepository.addOrder(order) 13 | } 14 | } -------------------------------------------------------------------------------- /src/order-management/domain/interfaces/order.interface.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '../order'; 2 | export interface IOrderRepository { 3 | addOrder(order: Order): Promise; 4 | } -------------------------------------------------------------------------------- /src/order-management/domain/order.ts: -------------------------------------------------------------------------------- 1 | export class Order { 2 | orderId: number; 3 | total: number; 4 | productId: number; 5 | createdAt: Date; 6 | 7 | constructor(productId: number, total: number) { 8 | 9 | if(total > 3){ 10 | throw new Error('No podes comprar mas de 2 productos') 11 | } 12 | 13 | this.orderId = -1; 14 | this.productId = productId; 15 | this.total = total; 16 | this.createdAt = new Date(); 17 | } 18 | } -------------------------------------------------------------------------------- /src/order-management/infraestructure/controllers/order.controller.ts: -------------------------------------------------------------------------------- 1 | import { OrderService } from "../../application/order.service"; 2 | import { Body, Controller, Post, Route } from "tsoa"; 3 | import { OrderRepositoryPrismaSqlite } from "../order.repository"; 4 | import { Order } from '../../domain/order'; 5 | 6 | @Route('order') 7 | export class OrderController extends Controller { 8 | private readonly orderService: OrderService; 9 | 10 | constructor() { 11 | super(); 12 | var orderRepository = new OrderRepositoryPrismaSqlite(); 13 | this.orderService = new OrderService(orderRepository) 14 | } 15 | 16 | @Post() 17 | public async addOrder( 18 | @Body() requestBody: {productId: number, total: number } ): Promise { 19 | const {total, productId} = requestBody; 20 | return await this.orderService.addOrder(productId, total); 21 | } 22 | } -------------------------------------------------------------------------------- /src/order-management/infraestructure/order.repository.ts: -------------------------------------------------------------------------------- 1 | import { IOrderRepository } from '../domain/interfaces/order.interface'; 2 | import { Order } from '../domain/order'; 3 | import prisma from '../../../prisma/index'; 4 | 5 | export class OrderRepositoryPrismaSqlite implements IOrderRepository { 6 | public async addOrder(order: Order): Promise { 7 | return await prisma.order.create({ 8 | data: { 9 | total: order.total, 10 | productId: order.productId 11 | } 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /src/product-management/application/product.service.ts: -------------------------------------------------------------------------------- 1 | import { IProductRepository } from '../domain/interfaces/product.interface'; 2 | import { Product } from '../domain/product'; 3 | 4 | export class ProductService { 5 | private productRepository: IProductRepository; 6 | 7 | constructor(productRepository: IProductRepository){ 8 | this.productRepository = productRepository; 9 | } 10 | 11 | public async addProduct(name: string, price: number): Promise { 12 | const product: Product = new Product(name, price); 13 | return await this.productRepository.addProduct(product) 14 | } 15 | } -------------------------------------------------------------------------------- /src/product-management/domain/interfaces/product.interface.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../product"; 2 | 3 | export interface IProductRepository{ 4 | addProduct(product: Product): Promise 5 | } -------------------------------------------------------------------------------- /src/product-management/domain/product.ts: -------------------------------------------------------------------------------- 1 | export class Product{ 2 | productId: number 3 | name: string 4 | price: number 5 | 6 | constructor(name: string, price: number){ 7 | 8 | if(price < 0){ 9 | throw new Error("El precio no puede ser negativo") 10 | } 11 | 12 | 13 | this.productId = -1; 14 | this.name = name; 15 | this.price = price; 16 | } 17 | } -------------------------------------------------------------------------------- /src/product-management/infraestructure/controllers/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { ProductService } from "../../application/product.service"; 2 | import { Body, Controller, Post, Route } from "tsoa"; 3 | import { ProductRepositoryPrismaSqlite } from "../product.repository"; 4 | import { Product } from "../../domain/product"; 5 | 6 | @Route('product') 7 | export class ProductController extends Controller { 8 | private readonly productService: ProductService; 9 | 10 | constructor() { 11 | super(); 12 | const productService = new ProductRepositoryPrismaSqlite(); 13 | this.productService = new ProductService(productService) 14 | } 15 | 16 | @Post() 17 | public async addProduct( 18 | @Body() requestBody: {name: string, price: number } ): Promise { 19 | const {name, price} = requestBody; 20 | return await this.productService.addProduct(name, price); 21 | } 22 | } -------------------------------------------------------------------------------- /src/product-management/infraestructure/product.repository.ts: -------------------------------------------------------------------------------- 1 | import prisma from '../../../prisma'; 2 | import { IProductRepository } from '../domain/interfaces/product.interface'; 3 | import { Product } from '../domain/product'; 4 | 5 | export class ProductRepositoryPrismaSqlite implements IProductRepository { 6 | public async addProduct(product: Product): Promise { 7 | return await prisma.product.create({ 8 | data: { 9 | name: product.name, 10 | price: product.price 11 | } 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from "./app"; 2 | 3 | const port = process.env.PORT || 3000; 4 | 5 | app.listen(port, () => 6 | console.log(`Example app listening at http://localhost:${port}`) 7 | ); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "strict": true, 11 | 12 | /* Basic Options */ 13 | "incremental": true, 14 | 15 | /* Strict Type-Checking Options */ 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "strictBindCallApply": true, 20 | "strictPropertyInitialization": true, 21 | "noImplicitThis": true, 22 | "alwaysStrict": true, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noImplicitReturns": true, 28 | "noFallthroughCasesInSwitch": true, 29 | 30 | /* Module Resolution Options */ 31 | "baseUrl": ".", 32 | 33 | /* Experimental Options */ 34 | "experimentalDecorators": true, 35 | "emitDecoratorMetadata": true, 36 | 37 | /* Advanced Options */ 38 | "forceConsistentCasingInFileNames": true, 39 | 40 | "resolveJsonModule": true 41 | }, 42 | "include": ["src", "__test__"], 43 | } 44 | -------------------------------------------------------------------------------- /tsoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFile": "src/app.ts", 3 | "noImplicitAdditionalProperties": "throw-on-extras", 4 | "controllerPathGlobs": [ 5 | "src/**/*.controller.ts" 6 | ], 7 | "spec": { 8 | "outputDirectory": "build", 9 | "specVersion": 3 10 | }, 11 | "routes": { 12 | "routesDir": "build" 13 | } 14 | } --------------------------------------------------------------------------------