├── .env.example ├── .gitattributes ├── prisma ├── dev.db ├── index.ts ├── singleton.jest.ts └── schema.prisma ├── nodemon.json ├── src ├── order-management │ ├── domain │ │ ├── interfaces │ │ │ └── order.interface.ts │ │ └── order.ts │ ├── infraestructure │ │ ├── order.repository.ts │ │ └── controllers │ │ │ └── order.controller.ts │ └── application │ │ └── order.service.ts ├── product-management │ ├── domain │ │ ├── interfaces │ │ │ └── product.interface.ts │ │ └── product.ts │ ├── infraestructure │ │ ├── product.repository.ts │ │ └── controllers │ │ │ └── product.controller.ts │ └── application │ │ └── product.service.ts ├── server.ts └── app.ts ├── jest.config.ts ├── .eslintrc.js ├── tsoa.json ├── __test__ ├── order.service.test.ts └── product.service.test.ts ├── tsconfig.json ├── package.json ├── README.md └── .gitignore /.env.example: -------------------------------------------------------------------------------- 1 | PORT = 3001 2 | 3 | # sqlite path 4 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperMartin/architecture-hexagonal-node/HEAD/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/order-management/domain/interfaces/order.interface.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '../order'; 2 | export interface IOrderRepository { 3 | addOrder(order: Order): Promise; 4 | } -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /__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 | }) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | --------------------------------------------------------------------------------