├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── api.http ├── apps ├── mailer │ ├── src │ │ ├── mailer.consumer.ts │ │ ├── mailer.controller.spec.ts │ │ ├── mailer.module.ts │ │ ├── main.ts │ │ └── rabbitmq │ │ │ └── rabbitmq.module.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ └── tsconfig.app.json └── orders │ ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── application │ │ └── application.module.ts │ ├── customer │ │ ├── customers.controller.spec.ts │ │ ├── customers.controller.ts │ │ ├── customers.module.ts │ │ ├── customers.service.spec.ts │ │ ├── customers.service.ts │ │ ├── domain-events │ │ │ └── customer-created.event.ts │ │ ├── dto │ │ │ ├── create-customer.dto.ts │ │ │ └── update-customer.dto.ts │ │ └── entities │ │ │ ├── customer.entity.ts │ │ │ └── customer.repository.ts │ ├── events │ │ ├── events.module.ts │ │ ├── integration-events-publisher.job.ts │ │ └── integration-events-queue.publisher.ts │ ├── invoices │ │ ├── domain-events │ │ │ ├── invoice-created.event.ts │ │ │ └── invoice-payed.event.ts │ │ ├── entities │ │ │ ├── invoice.entity.ts │ │ │ └── invoice.repository.ts │ │ ├── handlers │ │ │ └── process-payment.handler.ts │ │ ├── invoices.module.ts │ │ └── payment.gateway.ts │ ├── main.ts │ ├── orders │ │ ├── domain-events │ │ │ ├── order-approved.event.ts │ │ │ ├── order-created.event.ts │ │ │ └── order-reject.event.ts │ │ ├── dto │ │ │ ├── create-order.dto.ts │ │ │ └── update-order.dto.ts │ │ ├── entities │ │ │ ├── order.entity.ts │ │ │ └── order.repository.ts │ │ ├── handlers │ │ │ └── approve-order.handler.ts │ │ ├── integration-events │ │ │ └── order-approved.int-event.ts │ │ ├── orders.controller.spec.ts │ │ ├── orders.controller.ts │ │ ├── orders.module.ts │ │ ├── orders.service.spec.ts │ │ └── orders.service.ts │ ├── products │ │ ├── domain-events │ │ │ └── product-created.event.ts │ │ ├── dto │ │ │ ├── create-product.dto.ts │ │ │ └── update-product.dto.ts │ │ ├── entities │ │ │ ├── product.entity.ts │ │ │ └── product.repository.ts │ │ ├── products.controller.spec.ts │ │ ├── products.controller.ts │ │ ├── products.module.ts │ │ ├── products.service.spec.ts │ │ └── products.service.ts │ └── rabbitmq │ │ └── rabbitmq.module.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ └── tsconfig.app.json ├── docker-compose.yaml ├── libs └── @shared │ ├── src │ ├── application │ │ ├── application-service.ts │ │ └── domain-event-handler.interface.ts │ ├── domain │ │ ├── aggregate-root.ts │ │ ├── domain-event-manager.ts │ │ ├── domain-event.ts │ │ ├── entity.ts │ │ ├── integration-event.interface.ts │ │ ├── repository-interface.ts │ │ ├── stored-event.repository.ts │ │ └── stored-event.ts │ └── index.ts │ └── tsconfig.lib.json ├── mikro-orm.config.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .history/ 38 | .docker/dbdata/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | 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). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /api.http: -------------------------------------------------------------------------------- 1 | ### Products 2 | GET http://localhost:3000/products 3 | 4 | ### 5 | POST http://localhost:3000/products 6 | Content-Type: application/json 7 | 8 | { 9 | "name": "Product 1", 10 | "quantity": 10, 11 | "price": 100 12 | } 13 | 14 | ### 15 | @product_id = 7f485799-dad6-4b26-a1c3-1fb960f25e88 16 | 17 | ### Customers 18 | GET http://localhost:3000/customers 19 | 20 | ### 21 | POST http://localhost:3000/customers 22 | Content-Type: application/json 23 | 24 | { 25 | "name": "Customer 1", 26 | "email": "customer@customer.com", 27 | "address": "Customer Address", 28 | "phone": "123456789" 29 | } 30 | 31 | ### 32 | @customer_id = 7fd77a71-a9af-4d97-938a-f8ba2f0d3a0e 33 | 34 | ### Orders 35 | GET http://localhost:3000/orders 36 | 37 | ### 38 | POST http://localhost:3000/orders 39 | Content-Type: application/json 40 | 41 | { 42 | "customer_id": "{{customer_id}}", 43 | "items": [ 44 | { 45 | "product_id": "{{product_id}}", 46 | "quantity": 1 47 | } 48 | ] 49 | } 50 | 51 | # orders --- sincrono (criacao do pedido, criacao da fatura, pagamento) 52 | # processos de longa duração - 53 | # order orquestrator service 54 | # order created -> invoice created -> invoice payed -> order approved 55 | # -> invoice rejected -> order canceled -> enviar mail 56 | 57 | # orquestação e coerografia 58 | 59 | # orders -- assincrona - enviar o evento de integração via rabbitmq --- mailer 60 | 61 | ## processar o pedido 62 | 63 | #OrderCreated 64 | #InvoiceCreated 65 | #InvoicePayed 66 | #OrderApproved 67 | 68 | 69 | //--- 70 | #DebitEvent 71 | #CreditEvent - 31/01/2023 (saldo daquele dia) 72 | # snapshot (instantaneo) Conta - 1000 73 | #CreditEvent 74 | #DebitEvent 75 | # 76 | # 77 | # 78 | # - 20/06/2023 -------------------------------------------------------------------------------- /apps/mailer/src/mailer.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OrderApprovedIntegrationEvent } from '../../orders/src/orders/integration-events/order-approved.int-event'; 3 | import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; 4 | 5 | @Injectable() 6 | export class MailerConsumer { 7 | @RabbitSubscribe({ 8 | exchange: 'amq.direct', 9 | routingKey: OrderApprovedIntegrationEvent.name, 10 | //routingKey: 'events.fullcycle.com/*', 11 | queue: 'mailer', 12 | }) 13 | consume(msg: { event_name: string; [key: string]: any }) { 14 | console.log('Received message:', msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/mailer/src/mailer.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MailerController } from './mailer.controller'; 3 | import { MailerService } from './mailer.consumer'; 4 | 5 | describe('MailerController', () => { 6 | let mailerController: MailerController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [MailerController], 11 | providers: [MailerService], 12 | }).compile(); 13 | 14 | mailerController = app.get(MailerController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(mailerController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/mailer/src/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerConsumer } from './mailer.consumer'; 3 | import { RabbitmqModule } from './rabbitmq/rabbitmq.module'; 4 | 5 | @Module({ 6 | imports: [RabbitmqModule], 7 | controllers: [], 8 | providers: [MailerConsumer], 9 | }) 10 | export class MailerModule {} 11 | -------------------------------------------------------------------------------- /apps/mailer/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { MailerModule } from './mailer.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.createApplicationContext(MailerModule); 6 | await app.init(); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /apps/mailer/src/rabbitmq/rabbitmq.module.ts: -------------------------------------------------------------------------------- 1 | import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | @Global() 5 | @Module({ 6 | imports: [ 7 | RabbitMQModule.forRoot(RabbitMQModule, { 8 | uri: 'amqp://admin:admin@localhost:5672', 9 | connectionInitOptions: { wait: false }, 10 | }), 11 | RabbitmqModule, 12 | ], 13 | exports: [RabbitMQModule], 14 | }) 15 | export class RabbitmqModule {} 16 | -------------------------------------------------------------------------------- /apps/mailer/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { MailerModule } from './../src/mailer.module'; 5 | 6 | describe('MailerController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [MailerModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/mailer/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/mailer/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/mailer" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/orders/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/orders/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/orders/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { OrdersModule } from './orders/orders.module'; 5 | import { ProductsModule } from './products/products.module'; 6 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 7 | import { Product } from './products/entities/product.entity'; 8 | import { EventsModule } from './events/events.module'; 9 | import { CustomersModule } from './customer/customers.module'; 10 | import { Customer } from './customer/entities/customer.entity'; 11 | import { ApplicationModule } from './application/application.module'; 12 | import { Order, OrderItem } from './orders/entities/order.entity'; 13 | import { Invoice } from './invoices/entities/invoice.entity'; 14 | import { InvoicesModule } from './invoices/invoices.module'; 15 | import { BullModule } from '@nestjs/bull'; 16 | import { RabbitmqModule } from './rabbitmq/rabbitmq.module'; 17 | import { StoredEvent } from '@shared'; 18 | 19 | @Module({ 20 | imports: [ 21 | MikroOrmModule.forRoot({ 22 | entities: [Product, Customer, Order, OrderItem, Invoice, StoredEvent], 23 | dbName: 'nest', 24 | host: 'localhost', 25 | user: 'root', 26 | password: 'root', 27 | type: 'mysql', 28 | }), 29 | BullModule.forRoot({ 30 | redis: { 31 | host: 'localhost', 32 | port: 6379, 33 | }, 34 | }), 35 | RabbitmqModule, 36 | OrdersModule, 37 | ProductsModule, 38 | EventsModule, 39 | CustomersModule, 40 | ApplicationModule, 41 | InvoicesModule, 42 | ], 43 | controllers: [AppController], 44 | providers: [AppService], 45 | }) 46 | export class AppModule {} 47 | -------------------------------------------------------------------------------- /apps/orders/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/orders/src/application/application.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module, Scope } from '@nestjs/common'; 2 | import { ApplicationService } from '@shared'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [ 7 | { 8 | provide: ApplicationService, 9 | useClass: ApplicationService, 10 | scope: Scope.REQUEST, 11 | }, 12 | ], 13 | exports: [ApplicationService], 14 | }) 15 | export class ApplicationModule {} 16 | -------------------------------------------------------------------------------- /apps/orders/src/customer/customers.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CustomersController } from './customers.controller'; 3 | import { CustomersService } from './customers.service'; 4 | 5 | describe('CustomersController', () => { 6 | let controller: CustomersController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [CustomersController], 11 | providers: [CustomersService], 12 | }).compile(); 13 | 14 | controller = module.get(CustomersController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/orders/src/customer/customers.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { CustomersService } from './customers.service'; 11 | import { CreateCustomerDto } from './dto/create-customer.dto'; 12 | import { UpdateCustomerDto } from './dto/update-customer.dto'; 13 | 14 | @Controller('customers') 15 | export class CustomersController { 16 | constructor(private readonly customerService: CustomersService) {} 17 | 18 | @Post() 19 | create(@Body() createCustomerDto: CreateCustomerDto) { 20 | return this.customerService.create(createCustomerDto); 21 | } 22 | 23 | @Get() 24 | findAll() { 25 | return this.customerService.findAll(); 26 | } 27 | 28 | @Get(':id') 29 | findOne(@Param('id') id: string) { 30 | return this.customerService.findOne(+id); 31 | } 32 | 33 | @Patch(':id') 34 | update( 35 | @Param('id') id: string, 36 | @Body() updateCustomerDto: UpdateCustomerDto, 37 | ) { 38 | return this.customerService.update(+id, updateCustomerDto); 39 | } 40 | 41 | @Delete(':id') 42 | remove(@Param('id') id: string) { 43 | return this.customerService.remove(+id); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/orders/src/customer/customers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CustomersService } from './customers.service'; 3 | import { CustomersController } from './customers.controller'; 4 | import { CustomerRepository } from './entities/customer.repository'; 5 | import { Customer } from './entities/customer.entity'; 6 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 7 | import { DomainEventManager } from '@shared'; 8 | import { CustomerCreatedEvent } from './domain-events/customer-created.event'; 9 | 10 | @Module({ 11 | imports: [MikroOrmModule.forFeature([Customer])], 12 | controllers: [CustomersController], 13 | providers: [CustomersService, CustomerRepository], 14 | }) 15 | export class CustomersModule { 16 | constructor(private readonly domainEventManager: DomainEventManager) {} 17 | 18 | onModuleInit() { 19 | this.domainEventManager.register( 20 | CustomerCreatedEvent.name, 21 | async (event: CustomerCreatedEvent) => { 22 | console.log('CustomerCreatedEvent', event); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/orders/src/customer/customers.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CustomersService } from './customers.service'; 3 | 4 | describe('CustomersService', () => { 5 | let service: CustomersService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CustomersService], 10 | }).compile(); 11 | 12 | service = module.get(CustomersService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/orders/src/customer/customers.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateCustomerDto } from './dto/create-customer.dto'; 3 | import { UpdateCustomerDto } from './dto/update-customer.dto'; 4 | import { CustomerRepository } from './entities/customer.repository'; 5 | import { ApplicationService } from '@shared'; 6 | import { Customer } from './entities/customer.entity'; 7 | 8 | @Injectable() 9 | export class CustomersService { 10 | constructor( 11 | private customerRepo: CustomerRepository, 12 | private appService: ApplicationService, 13 | ) {} 14 | 15 | create(createCustomerDto: CreateCustomerDto) { 16 | return this.appService.run(async () => { 17 | const customer = Customer.create({ 18 | name: createCustomerDto.name, 19 | email: createCustomerDto.email, 20 | phone: createCustomerDto.phone, 21 | address: createCustomerDto.address, 22 | }); 23 | this.customerRepo.add(customer); 24 | return customer; 25 | }); 26 | } 27 | 28 | findAll() { 29 | return this.customerRepo.findAll(); 30 | } 31 | 32 | findOne(id: number) { 33 | return `This action returns a #${id} customer`; 34 | } 35 | 36 | update(id: number, updateCustomerDto: UpdateCustomerDto) { 37 | return `This action updates a #${id} customer`; 38 | } 39 | 40 | remove(id: number) { 41 | return `This action removes a #${id} customer`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/orders/src/customer/domain-events/customer-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | 3 | export class CustomerCreatedEvent implements IDomainEvent { 4 | occurred_on: Date; 5 | event_version: number; 6 | 7 | constructor( 8 | public aggregate_id: string, 9 | public name: string, 10 | public email: string, 11 | public phone: string, 12 | public address: string, 13 | ) { 14 | this.aggregate_id = aggregate_id; 15 | this.occurred_on = new Date(); 16 | this.event_version = 1; 17 | } 18 | } 19 | //event carried state transfer 20 | //event notification 21 | -------------------------------------------------------------------------------- /apps/orders/src/customer/dto/create-customer.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCustomerDto { 2 | name: string; 3 | email: string; 4 | address: string; 5 | phone: string; 6 | } 7 | -------------------------------------------------------------------------------- /apps/orders/src/customer/dto/update-customer.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateCustomerDto } from './create-customer.dto'; 3 | 4 | export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {} 5 | -------------------------------------------------------------------------------- /apps/orders/src/customer/entities/customer.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; 2 | import { AggregateRoot } from '@shared'; 3 | import { CustomerCreatedEvent } from '../domain-events/customer-created.event'; 4 | import crypto from 'crypto'; 5 | @Entity() 6 | export class Customer extends AggregateRoot { 7 | @PrimaryKey() 8 | id: string; 9 | 10 | @Property() 11 | name: string; 12 | 13 | @Property() 14 | email: string; 15 | 16 | @Property() 17 | address: string; 18 | 19 | @Property() 20 | phone: string; 21 | 22 | @Property() 23 | createdAt: Date = new Date(); 24 | 25 | @Property({ onUpdate: () => new Date() }) 26 | updatedAt: Date = new Date(); 27 | 28 | constructor( 29 | name: string, 30 | email: string, 31 | address: string, 32 | phone: string, 33 | id?: string, 34 | ) { 35 | super(); 36 | this.name = name; 37 | this.email = email; 38 | this.address = address; 39 | this.phone = phone; 40 | this.id = id ?? crypto.randomUUID(); 41 | } 42 | 43 | static create({ 44 | name, 45 | email, 46 | address, 47 | phone, 48 | }: { 49 | name: string; 50 | email: string; 51 | address: string; 52 | phone: string; 53 | }) { 54 | const customer = new Customer(name, email, address, phone); 55 | customer.addEvent( 56 | new CustomerCreatedEvent(customer.id, name, email, phone, address), 57 | ); 58 | return customer; 59 | } 60 | 61 | toJSON() { 62 | return { 63 | id: this.id, 64 | name: this.name, 65 | email: this.email, 66 | address: this.address, 67 | phone: this.phone, 68 | createdAt: this.createdAt, 69 | updatedAt: this.updatedAt, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /apps/orders/src/customer/entities/customer.repository.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@shared'; 2 | import { Customer } from './customer.entity'; 3 | import { EntityManager } from '@mikro-orm/mysql'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class CustomerRepository implements IRepository { 8 | constructor(private entityManager: EntityManager) {} 9 | 10 | async add(entity: Customer): Promise { 11 | this.entityManager.persist(entity); 12 | } 13 | 14 | findById(id: string): Promise { 15 | return this.entityManager.findOne(Customer, id); 16 | } 17 | 18 | findAll(): Promise { 19 | return this.entityManager.find(Customer, {}); 20 | } 21 | 22 | async delete(entity: Customer): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/orders/src/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 2 | import { Global, Module, OnModuleInit } from '@nestjs/common'; 3 | import { DomainEventManager, IDomainEvent } from '@shared'; 4 | import { StoredEvent } from '@shared'; 5 | import { StoredEventRepository } from '@shared'; 6 | import { ModuleRef } from '@nestjs/core'; 7 | import { BullModule } from '@nestjs/bull'; 8 | import { IntegrationEventsPublisherJob } from './integration-events-publisher.job'; 9 | import { IntegrationEventsQueuePublisher } from './integration-events-queue.publisher'; 10 | 11 | 12 | @Global() 13 | @Module({ 14 | imports: [ 15 | MikroOrmModule.forFeature([StoredEvent]), 16 | BullModule.registerQueue({ 17 | name: 'integration-events', 18 | }), 19 | ], 20 | providers: [ 21 | DomainEventManager, 22 | StoredEventRepository, 23 | IntegrationEventsPublisherJob, 24 | IntegrationEventsQueuePublisher, 25 | ], 26 | exports: [DomainEventManager, IntegrationEventsQueuePublisher], 27 | }) 28 | export class EventsModule implements OnModuleInit { 29 | constructor( 30 | private readonly domainEventManager: DomainEventManager, 31 | private moduleRef: ModuleRef, 32 | ) {} 33 | 34 | onModuleInit() { 35 | this.domainEventManager.register('*', async (event: IDomainEvent) => { 36 | const repo = await this.moduleRef.resolve(StoredEventRepository); 37 | await repo.add(event); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/orders/src/events/integration-events-publisher.job.ts: -------------------------------------------------------------------------------- 1 | import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; 2 | import { Job } from 'bull'; 3 | import { Process, Processor } from '@nestjs/bull'; 4 | import { IIntegrationEvent } from '@shared'; 5 | 6 | @Processor('integration-events') 7 | export class IntegrationEventsPublisherJob { 8 | constructor(private amqpConnection: AmqpConnection) {} 9 | 10 | @Process() 11 | async handle(job: Job) { 12 | console.log('IntegrationEventsPublisher.handle', job.data); 13 | await this.amqpConnection.publish( 14 | 'amq.direct', 15 | //events.fullcycle.com/MyEvent 16 | job.data.event_name, 17 | job.data, 18 | ); 19 | return {}; 20 | } 21 | } -------------------------------------------------------------------------------- /apps/orders/src/events/integration-events-queue.publisher.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { IIntegrationEvent } from '@shared'; 3 | import { Queue } from 'bull'; 4 | import { InjectQueue } from '@nestjs/bull'; 5 | 6 | @Injectable() 7 | export class IntegrationEventsQueuePublisher { 8 | constructor( 9 | @InjectQueue('integration-events') 10 | private queue: Queue, 11 | ) {} 12 | 13 | async addToQueue(integrationEvent: IIntegrationEvent) { 14 | await this.queue.add(integrationEvent); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/domain-events/invoice-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | import { InvoiceStatus } from '../entities/invoice.entity'; 3 | 4 | export class InvoiceCreatedEvent implements IDomainEvent { 5 | occurred_on: Date; 6 | event_version: number; 7 | 8 | constructor( 9 | public aggregate_id: string, 10 | public order_id: string, 11 | public status: InvoiceStatus, 12 | public amount: number, 13 | ) { 14 | this.aggregate_id = aggregate_id; 15 | this.occurred_on = new Date(); 16 | this.event_version = 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/domain-events/invoice-payed.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | 3 | export class InvoicePayedEvent implements IDomainEvent { 4 | occurred_on: Date; 5 | event_version: number; 6 | 7 | constructor( 8 | public aggregate_id: string, 9 | public order_id: string, 10 | public date: Date, 11 | ) { 12 | this.aggregate_id = aggregate_id; 13 | this.occurred_on = new Date(); 14 | this.event_version = 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/entities/invoice.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity as MikroEntity, 3 | Enum, 4 | ManyToOne, 5 | PrimaryKey, 6 | Property, 7 | } from '@mikro-orm/core'; 8 | import { AggregateRoot } from '@shared'; 9 | import { Order } from '../../orders/entities/order.entity'; 10 | import { InvoiceCreatedEvent } from '../domain-events/invoice-created.event'; 11 | import crypto from 'crypto'; 12 | import { InvoicePayedEvent } from '../domain-events/invoice-payed.event'; 13 | 14 | @MikroEntity() 15 | export class Invoice extends AggregateRoot { 16 | @PrimaryKey() 17 | id: string; 18 | 19 | @ManyToOne(() => Order, { mapToPk: true }) 20 | order_id: string; 21 | 22 | @Enum(() => InvoiceStatus) 23 | status: InvoiceStatus; 24 | 25 | @Property({ type: 'decimal' }) 26 | amount: number; 27 | 28 | @Property() 29 | createdAt: Date = new Date(); 30 | 31 | @Property({ onUpdate: () => new Date() }) 32 | updatedAt: Date = new Date(); 33 | 34 | constructor( 35 | order_id: string, 36 | amount: number, 37 | status: InvoiceStatus, 38 | id?: string, 39 | ) { 40 | super(); 41 | this.order_id = order_id; 42 | this.amount = amount; 43 | this.status = status; 44 | this.id = id ?? crypto.randomUUID(); 45 | } 46 | 47 | static create({ 48 | order_id, 49 | amount, 50 | status, 51 | }: { 52 | order_id: string; 53 | amount: number; 54 | status: InvoiceStatus; 55 | }) { 56 | const invoice = new Invoice(order_id, amount, status); 57 | invoice.addEvent( 58 | new InvoiceCreatedEvent(invoice.id, order_id, status, amount), 59 | ); 60 | return invoice; 61 | } 62 | 63 | pay() { 64 | this.status = InvoiceStatus.PAYED; 65 | this.addEvent(new InvoicePayedEvent(this.id, this.order_id, new Date())); 66 | } 67 | 68 | reject() { 69 | this.status = InvoiceStatus.REJECTED; 70 | } 71 | 72 | toJSON() { 73 | return { 74 | id: this.id, 75 | order_id: this.order_id, 76 | amount: this.amount, 77 | status: this.status, 78 | createdAt: this.createdAt, 79 | updatedAt: this.updatedAt, 80 | }; 81 | } 82 | } 83 | 84 | export enum InvoiceStatus { 85 | PENDING = 'PENDING', 86 | PAYED = 'PAYED', 87 | REJECTED = 'REJECTED', 88 | } 89 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/entities/invoice.repository.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@shared'; 2 | import { Invoice } from './invoice.entity'; 3 | import { EntityManager } from '@mikro-orm/mysql'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class InvoiceRepository implements IRepository { 8 | constructor(private entityManager: EntityManager) {} 9 | 10 | async add(entity: Invoice): Promise { 11 | this.entityManager.persist(entity); 12 | } 13 | 14 | findById(id: string): Promise { 15 | return this.entityManager.findOne(Invoice, id); 16 | } 17 | 18 | findAll(): Promise { 19 | return this.entityManager.find(Invoice, {}); 20 | } 21 | 22 | async delete(entity: Invoice): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/handlers/process-payment.handler.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { IDomainEventHandler } from '@shared'; 3 | import { DomainEventManager } from '@shared'; 4 | import { OrderCreatedEvent } from '../../orders/domain-events/order-created.event'; 5 | import { InvoiceRepository } from '../entities/invoice.repository'; 6 | import { PaymentGateway } from '../payment.gateway'; 7 | import { Invoice, InvoiceStatus } from '../entities/invoice.entity'; 8 | 9 | @Injectable() 10 | export class ProcessPaymentHandler implements IDomainEventHandler { 11 | constructor( 12 | private invoiceRepo: InvoiceRepository, 13 | private paymentGateway: PaymentGateway, 14 | private domainEventManager: DomainEventManager, 15 | ) {} 16 | 17 | async handle(event: OrderCreatedEvent): Promise { 18 | const invoice = Invoice.create({ 19 | order_id: event.aggregate_id, 20 | amount: event.items.reduce( 21 | (total, item) => total + item.price * item.quantity, 22 | 0, 23 | ), 24 | status: InvoiceStatus.PENDING, 25 | }); 26 | this.invoiceRepo.add(invoice); 27 | await this.paymentGateway.pay(invoice); 28 | invoice.pay(); 29 | //InvoiceCreated e InvoicePayed 30 | await this.domainEventManager.publish(invoice); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/invoices.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { PaymentGateway } from './payment.gateway'; 3 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 4 | import { Invoice } from './entities/invoice.entity'; 5 | import { InvoiceRepository } from './entities/invoice.repository'; 6 | import { ProcessPaymentHandler } from './handlers/process-payment.handler'; 7 | import { DomainEventManager } from '@shared'; 8 | import { OrderCreatedEvent } from '../orders/domain-events/order-created.event'; 9 | import { ModuleRef } from '@nestjs/core'; 10 | 11 | @Module({ 12 | imports: [MikroOrmModule.forFeature([Invoice])], 13 | providers: [PaymentGateway, InvoiceRepository, ProcessPaymentHandler], 14 | exports: [PaymentGateway, InvoiceRepository], 15 | }) 16 | export class InvoicesModule implements OnModuleInit { 17 | constructor( 18 | private readonly domainEventManager: DomainEventManager, 19 | private moduleRef: ModuleRef, 20 | ) {} 21 | 22 | onModuleInit() { 23 | this.domainEventManager.register( 24 | OrderCreatedEvent.name, 25 | async (event: OrderCreatedEvent) => { 26 | const handler: ProcessPaymentHandler = await this.moduleRef.resolve( 27 | ProcessPaymentHandler, 28 | ); 29 | await handler.handle(event); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/orders/src/invoices/payment.gateway.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Invoice } from './entities/invoice.entity'; 3 | 4 | @Injectable() 5 | export class PaymentGateway { 6 | pay(invoice: Invoice): Promise<{ transaction_id: number; error: any }> { 7 | return Promise.resolve({ transaction_id: 1, error: null }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/orders/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /apps/orders/src/orders/domain-events/order-approved.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | 3 | export class OrderApprovedEvent implements IDomainEvent { 4 | occurred_on: Date; 5 | event_version: number; 6 | 7 | constructor(public aggregate_id: string, public date: Date) { 8 | this.aggregate_id = aggregate_id; 9 | this.occurred_on = new Date(); 10 | this.event_version = 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/orders/src/orders/domain-events/order-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | import { OrderItem, OrderStatus } from '../entities/order.entity'; 3 | 4 | export class OrderCreatedEvent implements IDomainEvent { 5 | occurred_on: Date; 6 | event_version: number; 7 | 8 | constructor( 9 | public aggregate_id: string, 10 | public customer_id: string, 11 | public status: OrderStatus, 12 | public items: OrderItem[], 13 | ) { 14 | this.aggregate_id = aggregate_id; 15 | this.occurred_on = new Date(); 16 | this.event_version = 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/orders/src/orders/domain-events/order-reject.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | 3 | export class OrderRejectEvent implements IDomainEvent { 4 | occurred_on: Date; 5 | event_version: number; 6 | 7 | constructor(public aggregate_id: string, public date: Date) { 8 | this.aggregate_id = aggregate_id; 9 | this.occurred_on = new Date(); 10 | this.event_version = 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/orders/src/orders/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateOrderDto { 2 | customer_id: string; 3 | items: { product_id: string; quantity: number }[]; 4 | } 5 | -------------------------------------------------------------------------------- /apps/orders/src/orders/dto/update-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateOrderDto } from './create-order.dto'; 3 | 4 | export class UpdateOrderDto extends PartialType(CreateOrderDto) {} 5 | -------------------------------------------------------------------------------- /apps/orders/src/orders/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity as MikroEntity, 3 | Enum, 4 | ManyToOne, 5 | PrimaryKey, 6 | Property, 7 | OneToMany, 8 | Collection, 9 | Cascade, 10 | } from '@mikro-orm/core'; 11 | import { AggregateRoot, Entity } from '@shared'; 12 | import { Customer } from '../../customer/entities/customer.entity'; 13 | import { Product } from '../../products/entities/product.entity'; 14 | import { Invoice, InvoiceStatus } from '../../invoices/entities/invoice.entity'; 15 | import crypto from 'crypto'; 16 | import { OrderCreatedEvent } from '../domain-events/order-created.event'; 17 | import { OrderApprovedEvent } from '../domain-events/order-approved.event'; 18 | import { OrderRejectEvent } from '../domain-events/order-reject.event'; 19 | 20 | @MikroEntity() 21 | export class Order extends AggregateRoot { 22 | @PrimaryKey() 23 | id: string; 24 | 25 | @ManyToOne(() => Customer, { mapToPk: true }) 26 | customer_id: string; 27 | 28 | @Enum(() => OrderStatus) 29 | status: OrderStatus = OrderStatus.PENDING; 30 | 31 | @OneToMany(() => OrderItem, (item) => item.order_id, { 32 | eager: true, 33 | cascade: [Cascade.ALL], 34 | }) 35 | items = new Collection(this); 36 | 37 | @Property() 38 | createdAt: Date = new Date(); 39 | 40 | @Property({ onUpdate: () => new Date() }) 41 | updatedAt: Date = new Date(); 42 | 43 | constructor( 44 | customer_id: string, 45 | items?: OrderItem[], 46 | status?: OrderStatus, 47 | id?: string, 48 | ) { 49 | super(); 50 | this.customer_id = customer_id; 51 | this.items.add(items); 52 | if (status) { 53 | this.status = status; 54 | } 55 | this.id = id ?? crypto.randomUUID(); 56 | } 57 | 58 | static create({ 59 | customer_id, 60 | items, 61 | }: { 62 | customer_id: string; 63 | items: { product_id: string; quantity: number; price: number }[]; 64 | }) { 65 | const order = new Order(customer_id); 66 | const orderItems = items.map( 67 | (item) => 68 | new OrderItem(order.id, item.product_id, item.quantity, item.price), 69 | ); 70 | order.items.add(orderItems); 71 | order.addEvent( 72 | new OrderCreatedEvent(order.id, customer_id, order.status, orderItems), 73 | ); 74 | return order; 75 | } 76 | 77 | approve() { 78 | this.status = OrderStatus.APPROVED; 79 | this.addEvent(new OrderApprovedEvent(this.id, new Date())); 80 | } 81 | 82 | reject() { 83 | this.status = OrderStatus.REJECTED; 84 | this.addEvent(new OrderRejectEvent(this.id, new Date())); 85 | } 86 | 87 | createInvoice() { 88 | return Invoice.create({ 89 | order_id: this.id, 90 | amount: this.items 91 | .getItems() 92 | .reduce((total, item) => total + item.price * item.quantity, 0), 93 | status: InvoiceStatus.PENDING, 94 | }); 95 | } 96 | 97 | toJSON() { 98 | return { 99 | id: this.id, 100 | customer_id: this.customer_id, 101 | status: this.status, 102 | items: this.items.getItems().map((item) => item.toJSON()), 103 | createdAt: this.createdAt, 104 | updatedAt: this.updatedAt, 105 | }; 106 | } 107 | } 108 | 109 | export enum OrderStatus { 110 | PENDING = 'pending', 111 | APPROVED = 'approved', 112 | REJECTED = 'rejected', 113 | } 114 | 115 | @MikroEntity() 116 | export class OrderItem extends Entity { 117 | @PrimaryKey() 118 | id: number; 119 | 120 | @ManyToOne(() => Order, { mapToPk: true }) 121 | order_id: string; 122 | 123 | @ManyToOne(() => Product, { mapToPk: true }) 124 | product_id: string; 125 | 126 | @Property({ type: 'int' }) 127 | quantity: number; 128 | 129 | @Property({ type: 'decimal' }) 130 | price: number; 131 | 132 | constructor( 133 | order_id: string, 134 | product_id: string, 135 | quantity: number, 136 | price: number, 137 | id?: number, 138 | ) { 139 | super(); 140 | this.order_id = order_id; 141 | this.product_id = product_id; 142 | this.quantity = quantity; 143 | this.price = price; 144 | 145 | if (this.id) { 146 | this.id = id; 147 | } 148 | } 149 | 150 | toJSON() { 151 | return { 152 | id: this.id, 153 | order_id: this.order_id, 154 | product_id: this.product_id, 155 | quantity: this.quantity, 156 | price: this.price, 157 | }; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /apps/orders/src/orders/entities/order.repository.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@shared'; 2 | import { Order } from './order.entity'; 3 | import { EntityManager } from '@mikro-orm/mysql'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class OrderRepository implements IRepository { 8 | constructor(private entityManager: EntityManager) {} 9 | 10 | async add(entity: Order): Promise { 11 | this.entityManager.persist(entity); 12 | } 13 | 14 | findById(id: string): Promise { 15 | return this.entityManager.findOne(Order, id); 16 | } 17 | 18 | findAll(): Promise { 19 | return this.entityManager.find(Order, {}); 20 | } 21 | 22 | async delete(entity: Order): Promise { 23 | await this.entityManager.remove(entity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/orders/src/orders/handlers/approve-order.handler.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { IDomainEventHandler } from '@shared'; 3 | import { DomainEventManager } from '@shared'; 4 | import { InvoicePayedEvent } from '../../invoices/domain-events/invoice-payed.event'; 5 | import { OrderRepository } from '../entities/order.repository'; 6 | 7 | @Injectable() 8 | export class ApproveOrderHandler implements IDomainEventHandler { 9 | constructor( 10 | private orderRepo: OrderRepository, 11 | private domainEventManager: DomainEventManager, 12 | ) {} 13 | 14 | async handle(event: InvoicePayedEvent): Promise { 15 | const order = await this.orderRepo.findById(event.order_id); 16 | order.approve(); 17 | await this.domainEventManager.publish(order); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/orders/src/orders/integration-events/order-approved.int-event.ts: -------------------------------------------------------------------------------- 1 | import { IIntegrationEvent } from '@shared'; 2 | import { OrderApprovedEvent } from '../domain-events/order-approved.event'; 3 | 4 | export class OrderApprovedIntegrationEvent implements IIntegrationEvent { 5 | event_name: string; 6 | payload: any; 7 | event_version: number; 8 | occurred_on: Date; 9 | 10 | constructor(domainEvent: OrderApprovedEvent) { 11 | this.event_name = OrderApprovedIntegrationEvent.name; 12 | this.payload = { 13 | id: domainEvent.aggregate_id, 14 | date: domainEvent.occurred_on, 15 | }; //notification 16 | this.event_version = 1; 17 | this.occurred_on = domainEvent.occurred_on; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/orders/src/orders/orders.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OrdersController } from './orders.controller'; 3 | import { OrdersService } from './orders.service'; 4 | 5 | describe('OrdersController', () => { 6 | let controller: OrdersController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [OrdersController], 11 | providers: [OrdersService], 12 | }).compile(); 13 | 14 | controller = module.get(OrdersController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/orders/src/orders/orders.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; 2 | import { OrdersService } from './orders.service'; 3 | import { CreateOrderDto } from './dto/create-order.dto'; 4 | import { UpdateOrderDto } from './dto/update-order.dto'; 5 | 6 | @Controller('orders') 7 | export class OrdersController { 8 | constructor(private readonly ordersService: OrdersService) {} 9 | 10 | @Post() 11 | create(@Body() createOrderDto: CreateOrderDto) { 12 | return this.ordersService.process(createOrderDto); 13 | } 14 | 15 | @Get() 16 | findAll() { 17 | return this.ordersService.findAll(); 18 | } 19 | 20 | @Get(':id') 21 | findOne(@Param('id') id: string) { 22 | return this.ordersService.findOne(+id); 23 | } 24 | 25 | @Patch(':id') 26 | update(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) { 27 | return this.ordersService.update(+id, updateOrderDto); 28 | } 29 | 30 | @Delete(':id') 31 | remove(@Param('id') id: string) { 32 | return this.ordersService.remove(+id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/orders/src/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { OrdersService } from './orders.service'; 3 | import { OrdersController } from './orders.controller'; 4 | import { OrderRepository } from './entities/order.repository'; 5 | import { ProductsModule } from '../products/products.module'; 6 | import { Order, OrderItem } from './entities/order.entity'; 7 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 8 | import { InvoicesModule } from '../invoices/invoices.module'; 9 | import { ApproveOrderHandler } from './handlers/approve-order.handler'; 10 | import { DomainEventManager } from '@shared'; 11 | import { ModuleRef } from '@nestjs/core'; 12 | import { InvoicePayedEvent } from '../invoices/domain-events/invoice-payed.event'; 13 | import { OrderApprovedEvent } from './domain-events/order-approved.event'; 14 | import { OrderApprovedIntegrationEvent } from './integration-events/order-approved.int-event'; 15 | import { IntegrationEventsQueuePublisher } from '../events/integration-events-queue.publisher'; 16 | import EventEmitter2 from 'eventemitter2'; 17 | 18 | @Module({ 19 | imports: [ 20 | MikroOrmModule.forFeature([Order, OrderItem]), 21 | ProductsModule, 22 | InvoicesModule, 23 | ], 24 | controllers: [OrdersController], 25 | providers: [ 26 | OrdersService, 27 | OrderRepository, 28 | ApproveOrderHandler, 29 | { 30 | provide: EventEmitter2, 31 | useValue: new EventEmitter2(), 32 | }, 33 | ], 34 | }) 35 | export class OrdersModule implements OnModuleInit { 36 | constructor( 37 | private readonly domainEventManager: DomainEventManager, 38 | private moduleRef: ModuleRef, 39 | private integrationEventsQueue: IntegrationEventsQueuePublisher, 40 | private eventEmitter: EventEmitter2, 41 | ) {} 42 | 43 | onModuleInit() { 44 | this.domainEventManager.register( 45 | InvoicePayedEvent.name, 46 | async (event: InvoicePayedEvent) => { 47 | const handler: ApproveOrderHandler = await this.moduleRef.resolve( 48 | ApproveOrderHandler, 49 | ); 50 | await handler.handle(event); 51 | }, 52 | ); 53 | this.domainEventManager.registerForIntegrationEvent( 54 | OrderApprovedEvent.name, 55 | async (event) => { 56 | console.log('integration events'); 57 | const integrationEvent = new OrderApprovedIntegrationEvent(event); 58 | await this.integrationEventsQueue.addToQueue(integrationEvent); 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/orders/src/orders/orders.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OrdersService } from './orders.service'; 3 | 4 | describe('OrdersService', () => { 5 | let service: OrdersService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [OrdersService], 10 | }).compile(); 11 | 12 | service = module.get(OrdersService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/orders/src/orders/orders.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateOrderDto } from './dto/create-order.dto'; 3 | import { UpdateOrderDto } from './dto/update-order.dto'; 4 | import { OrderRepository } from './entities/order.repository'; 5 | import { ApplicationService } from '@shared'; 6 | import { Order } from './entities/order.entity'; 7 | import { ProductRepository } from '../products/entities/product.repository'; 8 | import { InvoiceRepository } from '../invoices/entities/invoice.repository'; 9 | import { PaymentGateway } from '../invoices/payment.gateway'; 10 | 11 | @Injectable() 12 | export class OrdersService { 13 | constructor( 14 | private orderRepo: OrderRepository, 15 | private productRepo: ProductRepository, 16 | private paymentGateway: PaymentGateway, 17 | private invoiceRepo: InvoiceRepository, 18 | private appService: ApplicationService, 19 | ) {} 20 | 21 | async process(createOrderDto: CreateOrderDto) { 22 | const products = await this.productRepo.findByIds( 23 | createOrderDto.items.map((item) => item.product_id), 24 | ); 25 | 26 | const items = createOrderDto.items.map((item) => ({ 27 | product_id: item.product_id, 28 | quantity: item.quantity, 29 | price: products.find((product) => product.id === item.product_id).price, 30 | })); 31 | 32 | return this.appService.run(async () => { 33 | const order = Order.create({ 34 | customer_id: createOrderDto.customer_id, 35 | items, 36 | }); 37 | this.orderRepo.add(order); 38 | return order; 39 | }); 40 | } 41 | 42 | findAll() { 43 | return this.orderRepo.findAll(); 44 | } 45 | 46 | findOne(id: number) { 47 | return `This action returns a #${id} order`; 48 | } 49 | 50 | update(id: number, updateOrderDto: UpdateOrderDto) { 51 | return `This action updates a #${id} order`; 52 | } 53 | 54 | remove(id: number) { 55 | return `This action removes a #${id} order`; 56 | } 57 | } 58 | 59 | //procedural 60 | // const order = Order.create({ 61 | // customer_id: createOrderDto.customer_id, 62 | // items, 63 | // }); 64 | // this.orderRepo.add(order); 65 | // const invoice = order.createInvoice(); 66 | // this.invoiceRepo.add(invoice); 67 | // const payment = await this.paymentGateway.pay(invoice); 68 | // if (!payment.error) { 69 | // invoice.pay(); 70 | // order.approve(); 71 | // } else { 72 | // invoice.reject(); 73 | // order.reject(); 74 | // } 75 | // //enviar e-mail 76 | // return order; 77 | 78 | //via eventos 79 | // const order = Order.create({ 80 | // customer_id: createOrderDto.customer_id, 81 | // items, 82 | // }); 83 | // this.orderRepo.add(order); 84 | // return order; 85 | -------------------------------------------------------------------------------- /apps/orders/src/products/domain-events/product-created.event.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@shared'; 2 | 3 | export class ProductCreatedEvent implements IDomainEvent { 4 | occurred_on: Date; 5 | event_version: number; 6 | 7 | constructor( 8 | public aggregate_id: string, 9 | public name: string, 10 | public price: number, 11 | public quantity: number, 12 | ) { 13 | this.aggregate_id = aggregate_id; 14 | this.occurred_on = new Date(); 15 | this.event_version = 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/orders/src/products/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateProductDto { 2 | name: string; 3 | price: number; 4 | quantity: number; 5 | } 6 | -------------------------------------------------------------------------------- /apps/orders/src/products/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateProductDto } from './create-product.dto'; 3 | 4 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 5 | -------------------------------------------------------------------------------- /apps/orders/src/products/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; 2 | import { AggregateRoot } from '@shared'; 3 | import { ProductCreatedEvent } from '../domain-events/product-created.event'; 4 | import crypto from 'crypto'; 5 | 6 | @Entity() 7 | export class Product extends AggregateRoot { 8 | @PrimaryKey() 9 | id: string; 10 | 11 | @Property() 12 | name: string; 13 | 14 | @Property({ columnType: 'decimal' }) 15 | price: number; 16 | 17 | @Property({ columnType: 'int' }) 18 | quantity: number; 19 | 20 | @Property() 21 | createdAt: Date = new Date(); 22 | 23 | @Property({ onUpdate: () => new Date() }) 24 | updatedAt: Date = new Date(); 25 | 26 | constructor(name: string, price: number, quantity: number, id?: string) { 27 | super(); 28 | this.name = name; 29 | this.price = price; 30 | this.quantity = quantity; 31 | this.id = id ?? crypto.randomUUID(); 32 | } 33 | 34 | static create({ 35 | name, 36 | price, 37 | quantity, 38 | }: { 39 | name: string; 40 | price: number; 41 | quantity: number; 42 | }) { 43 | const product = new Product(name, price, quantity); 44 | product.addEvent( 45 | new ProductCreatedEvent(product.id, name, price, quantity), 46 | ); 47 | return product; 48 | } 49 | 50 | toJSON() { 51 | return { 52 | id: this.id, 53 | name: this.name, 54 | quantity: this.quantity, 55 | createdAt: this.createdAt, 56 | updatedAt: this.updatedAt, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/orders/src/products/entities/product.repository.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@shared'; 2 | import { Product } from './product.entity'; 3 | import { EntityManager } from '@mikro-orm/mysql'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class ProductRepository implements IRepository { 8 | constructor(private entityManager: EntityManager) {} 9 | 10 | async add(entity: Product): Promise { 11 | this.entityManager.persist(entity); 12 | } 13 | 14 | findById(id: string): Promise { 15 | return this.entityManager.findOne(Product, id); 16 | } 17 | 18 | findByIds(ids: string[]): Promise { 19 | return this.entityManager.find(Product, { id: { $in: ids } }); 20 | } 21 | 22 | findAll(): Promise { 23 | return this.entityManager.find(Product, {}); 24 | } 25 | 26 | async delete(entity: Product): Promise { 27 | await this.entityManager.remove(entity); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/orders/src/products/products.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProductsController } from './products.controller'; 3 | import { ProductsService } from './products.service'; 4 | 5 | describe('ProductsController', () => { 6 | let controller: ProductsController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [ProductsController], 11 | providers: [ProductsService], 12 | }).compile(); 13 | 14 | controller = module.get(ProductsController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/orders/src/products/products.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { ProductsService } from './products.service'; 11 | import { CreateProductDto } from './dto/create-product.dto'; 12 | import { UpdateProductDto } from './dto/update-product.dto'; 13 | 14 | @Controller('products') 15 | export class ProductsController { 16 | constructor(private readonly productsService: ProductsService) {} 17 | 18 | @Post() 19 | create(@Body() createProductDto: CreateProductDto) { 20 | return this.productsService.create(createProductDto); 21 | } 22 | 23 | @Get() 24 | findAll() { 25 | return this.productsService.findAll(); 26 | } 27 | 28 | @Get(':id') 29 | findOne(@Param('id') id: string) { 30 | return this.productsService.findOne(+id); 31 | } 32 | 33 | @Patch(':id') 34 | update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) { 35 | return this.productsService.update(+id, updateProductDto); 36 | } 37 | 38 | @Delete(':id') 39 | remove(@Param('id') id: string) { 40 | return this.productsService.remove(+id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/orders/src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { ProductsService } from './products.service'; 3 | import { ProductsController } from './products.controller'; 4 | import { ProductRepository } from './entities/product.repository'; 5 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 6 | import { Product } from './entities/product.entity'; 7 | import { DomainEventManager } from '@shared'; 8 | import { ProductCreatedEvent } from './domain-events/product-created.event'; 9 | 10 | @Module({ 11 | imports: [MikroOrmModule.forFeature([Product])], 12 | controllers: [ProductsController], 13 | providers: [ProductsService, ProductRepository], 14 | exports: [ProductsService, ProductRepository], 15 | }) 16 | export class ProductsModule implements OnModuleInit { 17 | constructor(private readonly domainEventManager: DomainEventManager) {} 18 | 19 | onModuleInit() { 20 | this.domainEventManager.register( 21 | ProductCreatedEvent.name, 22 | async (event: ProductCreatedEvent) => { 23 | console.log('ProductCreatedEvent', event); 24 | }, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/orders/src/products/products.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProductsService } from './products.service'; 3 | 4 | describe('ProductsService', () => { 5 | let service: ProductsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProductsService], 10 | }).compile(); 11 | 12 | service = module.get(ProductsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/orders/src/products/products.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateProductDto } from './dto/create-product.dto'; 3 | import { UpdateProductDto } from './dto/update-product.dto'; 4 | import { Product } from './entities/product.entity'; 5 | import { ProductRepository } from './entities/product.repository'; 6 | import { ApplicationService } from '@shared'; 7 | 8 | @Injectable() 9 | export class ProductsService { 10 | constructor( 11 | private productRepo: ProductRepository, 12 | private appService: ApplicationService, 13 | ) {} 14 | 15 | create(createProductDto: CreateProductDto) { 16 | return this.appService.run(async () => { 17 | const product = Product.create({ 18 | name: createProductDto.name, 19 | price: createProductDto.price, 20 | quantity: createProductDto.quantity, 21 | }); 22 | this.productRepo.add(product); 23 | return product; 24 | }); 25 | } 26 | 27 | findAll() { 28 | return this.productRepo.findAll(); 29 | } 30 | 31 | findOne(id: number) { 32 | return `This action returns a #${id} product`; 33 | } 34 | 35 | update(id: number, updateProductDto: UpdateProductDto) { 36 | return `This action updates a #${id} product`; 37 | } 38 | 39 | remove(id: number) { 40 | return `This action removes a #${id} product`; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/orders/src/rabbitmq/rabbitmq.module.ts: -------------------------------------------------------------------------------- 1 | import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | @Global() 5 | @Module({ 6 | imports: [ 7 | RabbitMQModule.forRoot(RabbitMQModule, { 8 | uri: 'amqp://admin:admin@localhost:5672', 9 | connectionInitOptions: { wait: false }, 10 | }), 11 | RabbitmqModule, 12 | ], 13 | exports: [RabbitMQModule], 14 | }) 15 | export class RabbitmqModule {} 16 | -------------------------------------------------------------------------------- /apps/orders/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/orders/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | }, 13 | "moduleNameMapper": { 14 | "@shared/(.*)": "/../libs/@shared/src/$1", 15 | "@shared/": "/../libs/@shared/src" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/orders/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/orders" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:8.0.30-debian 6 | volumes: 7 | - ./.docker/dbdata:/var/lib/mysql 8 | ports: 9 | - 3306:3306 10 | environment: 11 | MYSQL_ROOT_PASSWORD: root 12 | MYSQL_DATABASE: nest 13 | 14 | redis: 15 | image: redis:7.0.8-alpine 16 | ports: 17 | - 6379:6379 18 | 19 | rabbitmq: 20 | image: rabbitmq:3.8-management-alpine 21 | ports: 22 | - 5672:5672 23 | - 15672:15672 24 | environment: 25 | - RABBITMQ_DEFAULT_USER=admin 26 | - RABBITMQ_DEFAULT_PASS=admin 27 | -------------------------------------------------------------------------------- /libs/@shared/src/application/application-service.ts: -------------------------------------------------------------------------------- 1 | import { DomainEventManager } from '../domain/domain-event-manager'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { AggregateRoot } from '../domain/aggregate-root'; 4 | import { EntityManager } from '@mikro-orm/mysql'; 5 | 6 | @Injectable() 7 | export class ApplicationService { 8 | constructor( 9 | private entityManager: EntityManager, 10 | private domainEventManager: DomainEventManager, 11 | ) {} 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-empty-function 14 | start() {} 15 | 16 | async finish() { 17 | const uow = this.entityManager.getUnitOfWork(); 18 | const aggregateRoots = [ 19 | //@ts-expect-error - entities are aggregateRoot in this context 20 | ...(uow.getPersistStack() as AggregateRoot[]), 21 | //@ts-expect-error - entities are aggregateRoot in this context 22 | ...(uow.getRemoveStack() as AggregateRoot[]), 23 | ]; 24 | for (const aggregateRoot of aggregateRoots) { 25 | await this.domainEventManager.publish(aggregateRoot); 26 | } 27 | await this.entityManager.flush(); 28 | for (const aggregateRoot of aggregateRoots) { 29 | await this.domainEventManager.publishForIntegrationEvent(aggregateRoot); 30 | } 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-empty-function 34 | fail() {} 35 | 36 | async run(callback: () => Promise): Promise { 37 | await this.start(); 38 | try { 39 | const result = await callback(); 40 | await this.finish(); 41 | return result; 42 | } catch (e) { 43 | await this.fail(); 44 | throw e; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/@shared/src/application/domain-event-handler.interface.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '../domain/domain-event'; 2 | 3 | export interface IDomainEventHandler { 4 | handle(event: IDomainEvent): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from './domain-event'; 2 | import { Entity } from './entity'; 3 | 4 | export abstract class AggregateRoot extends Entity { 5 | events: Set = new Set(); 6 | dispatchedEvents: Set = new Set(); 7 | 8 | addEvent(event: IDomainEvent) { 9 | this.events.add(event); 10 | } 11 | 12 | markEventsAsDispatched(event: IDomainEvent) { 13 | this.dispatchedEvents.add(event); 14 | } 15 | 16 | getUncommittedEvents(): IDomainEvent[] { 17 | return Array.from(this.events).filter((event) => { 18 | return !this.dispatchedEvents.has(event); 19 | }); 20 | } 21 | 22 | clearEvents() { 23 | this.events.clear(); 24 | this.dispatchedEvents.clear(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/domain-event-manager.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | import { AggregateRoot } from './aggregate-root'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class DomainEventManager { 7 | domainEventsSubscriber: EventEmitter2; //eventos que vão ser consumidos pela propria 8 | integrationEventsSubscriber: EventEmitter2; //eventos que vão ser consumidos por outros serviços 9 | 10 | constructor() { 11 | this.domainEventsSubscriber = new EventEmitter2({ 12 | wildcard: true, 13 | }); 14 | this.integrationEventsSubscriber = new EventEmitter2({ 15 | wildcard: true, 16 | }); 17 | } 18 | 19 | register(event: string, handler: any) { 20 | this.domainEventsSubscriber.on(event, handler); 21 | } 22 | 23 | registerForIntegrationEvent(event: string, handler: any) { 24 | this.integrationEventsSubscriber.on(event, handler); 25 | } 26 | 27 | async publish(aggregateRoot: AggregateRoot) { 28 | for (const event of aggregateRoot.getUncommittedEvents()) { 29 | const eventClassName = event.constructor.name; 30 | aggregateRoot.markEventsAsDispatched(event); 31 | await this.domainEventsSubscriber.emitAsync(eventClassName, event); 32 | } 33 | } 34 | 35 | async publishForIntegrationEvent(aggregateRoot: AggregateRoot) { 36 | for (const event of aggregateRoot.events) { 37 | const eventClassName = event.constructor.name; 38 | await this.integrationEventsSubscriber.emitAsync(eventClassName, event); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/domain-event.ts: -------------------------------------------------------------------------------- 1 | export interface IDomainEvent { 2 | aggregate_id: string; 3 | occurred_on: Date; 4 | event_version: number; 5 | } 6 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/entity.ts: -------------------------------------------------------------------------------- 1 | export abstract class Entity { 2 | readonly id: any; 3 | 4 | abstract toJSON(): any; 5 | 6 | equals(obj: this) { 7 | if (obj === null || obj === undefined) { 8 | return false; 9 | } 10 | 11 | if (obj.id === undefined) { 12 | return false; 13 | } 14 | 15 | if (obj.constructor.name !== this.constructor.name) { 16 | return false; 17 | } 18 | 19 | return obj.id.equals(this.id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/integration-event.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IIntegrationEvent { 2 | event_name: string; 3 | payload: T; 4 | event_version: number; 5 | occurred_on: Date; 6 | } 7 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from './aggregate-root'; 2 | 3 | export interface IRepository { 4 | add(entity: E): Promise; 5 | findById(id: any): Promise; 6 | findAll(): Promise; 7 | delete(entity: E): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/stored-event.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/mysql'; 2 | import { StoredEvent } from './stored-event'; 3 | import { IDomainEvent } from './domain-event'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class StoredEventRepository { 8 | constructor(private entityManager: EntityManager) {} 9 | 10 | allBetween(lowEventId: string, highEventId: string): Promise { 11 | return this.entityManager.find(StoredEvent, { 12 | id: { $gte: lowEventId, $lte: highEventId }, 13 | }); 14 | } 15 | 16 | allSince(eventId: string): Promise { 17 | return this.entityManager.find(StoredEvent, { id: { $gte: eventId } }); 18 | } 19 | 20 | add(domainEvent: IDomainEvent): StoredEvent { 21 | const storedEvent = StoredEvent.create(domainEvent); 22 | this.entityManager.persist(storedEvent); 23 | return storedEvent; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /libs/@shared/src/domain/stored-event.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; 2 | import { AggregateRoot } from './aggregate-root'; 3 | import { IDomainEvent } from './domain-event'; 4 | import crypto from 'crypto'; 5 | 6 | @Entity() 7 | export class StoredEvent extends AggregateRoot { 8 | @PrimaryKey() 9 | id: string; 10 | 11 | @Property({ type: 'json' }) 12 | body: string; 13 | 14 | @Property() 15 | type_name: string; 16 | 17 | @Property() 18 | occurred_on: Date; 19 | 20 | constructor(body: string, type_name: string, occurred_on: Date, id?: string) { 21 | super(); 22 | this.body = body; 23 | this.occurred_on = occurred_on; 24 | this.type_name = type_name; 25 | this.id = id ?? crypto.randomUUID(); 26 | } 27 | 28 | static create(domainEvent: IDomainEvent) { 29 | return new StoredEvent( 30 | JSON.stringify(domainEvent), 31 | domainEvent.constructor.name, 32 | domainEvent.occurred_on, 33 | ); 34 | } 35 | 36 | toJSON() { 37 | return { 38 | id: this.id, 39 | body: this.body, 40 | ocurred_on: this.occurred_on, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libs/@shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './domain/aggregate-root'; 2 | export * from './domain/domain-event-manager'; 3 | export * from './domain/domain-event'; 4 | export * from './domain/entity'; 5 | export * from './domain/repository-interface'; 6 | export * from './domain/integration-event.interface'; 7 | export * from './domain/repository-interface'; 8 | export * from './domain/stored-event.repository'; 9 | export * from './domain/stored-event'; 10 | export * from './application/application-service'; 11 | export * from './application/domain-event-handler.interface'; 12 | -------------------------------------------------------------------------------- /libs/@shared/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/@shared" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /mikro-orm.config.ts: -------------------------------------------------------------------------------- 1 | import { Customer } from './apps/orders/src/customer/entities/customer.entity'; 2 | import { Invoice } from './apps/orders/src/invoices/entities/invoice.entity'; 3 | import { 4 | Order, 5 | OrderItem, 6 | } from './apps/orders/src/orders/entities/order.entity'; 7 | import { Product } from './apps/orders/src/products/entities/product.entity'; 8 | import { StoredEvent } from './libs/@shared/src/domain/stored-event'; 9 | 10 | export default { 11 | entities: [Product, Customer, Order, OrderItem, Invoice, StoredEvent], 12 | dbName: 'nest', 13 | host: 'localhost', 14 | user: 'root', 15 | password: 'root', 16 | type: 'mysql', 17 | }; 18 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "apps/orders/src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "webpack": true, 8 | "tsConfigPath": "apps/orders/tsconfig.app.json" 9 | }, 10 | "projects": { 11 | "@shared": { 12 | "type": "library", 13 | "root": "libs/@shared", 14 | "entryFile": "index", 15 | "sourceRoot": "libs/@shared/src", 16 | "compilerOptions": { 17 | "tsConfigPath": "libs/@shared/tsconfig.lib.json" 18 | } 19 | }, 20 | "orders": { 21 | "type": "application", 22 | "root": "apps/orders", 23 | "entryFile": "main", 24 | "sourceRoot": "apps/orders/src", 25 | "compilerOptions": { 26 | "tsConfigPath": "apps/orders/tsconfig.app.json" 27 | } 28 | }, 29 | "mailer": { 30 | "type": "application", 31 | "root": "apps/mailer", 32 | "entryFile": "main", 33 | "sourceRoot": "apps/mailer/src", 34 | "compilerOptions": { 35 | "tsConfigPath": "apps/mailer/tsconfig.app.json" 36 | } 37 | } 38 | }, 39 | "monorepo": true, 40 | "root": "apps/orders" 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live-eda-test", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/apps/orders/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./apps/orders/test/jest-e2e.json", 21 | "refresh-db": "mikro-orm schema:fresh --run" 22 | }, 23 | "dependencies": { 24 | "@golevelup/nestjs-rabbitmq": "^4.0.0", 25 | "@mikro-orm/cli": "^5.7.14", 26 | "@mikro-orm/core": "^5.7.14", 27 | "@mikro-orm/mysql": "^5.7.14", 28 | "@mikro-orm/nestjs": "^5.2.1", 29 | "@nestjs/bull": "^10.0.1", 30 | "@nestjs/common": "^10.0.0", 31 | "@nestjs/core": "^10.0.0", 32 | "@nestjs/mapped-types": "*", 33 | "@nestjs/microservices": "^10.2.1", 34 | "@nestjs/platform-express": "^10.0.0", 35 | "bull": "^4.11.3", 36 | "eventemitter2": "^6.4.9", 37 | "reflect-metadata": "^0.1.13", 38 | "rxjs": "^7.8.1" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^10.0.0", 42 | "@nestjs/schematics": "^10.0.0", 43 | "@nestjs/testing": "^10.0.0", 44 | "@types/express": "^4.17.17", 45 | "@types/jest": "^29.5.2", 46 | "@types/node": "^20.3.1", 47 | "@types/supertest": "^2.0.12", 48 | "@typescript-eslint/eslint-plugin": "^5.59.11", 49 | "@typescript-eslint/parser": "^5.59.11", 50 | "eslint": "^8.42.0", 51 | "eslint-config-prettier": "^8.8.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "jest": "^29.5.0", 54 | "prettier": "^2.8.8", 55 | "source-map-support": "^0.5.21", 56 | "supertest": "^6.3.3", 57 | "ts-jest": "^29.1.0", 58 | "ts-loader": "^9.4.3", 59 | "ts-node": "^10.9.1", 60 | "tsconfig-paths": "^4.2.0", 61 | "typescript": "^5.1.3" 62 | }, 63 | "jest": { 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "ts" 68 | ], 69 | "rootDir": ".", 70 | "testRegex": ".*\\.spec\\.ts$", 71 | "transform": { 72 | "^.+\\.(t|j)s$": "ts-jest" 73 | }, 74 | "collectCoverageFrom": [ 75 | "**/*.(t|j)s" 76 | ], 77 | "coverageDirectory": "./coverage", 78 | "testEnvironment": "node", 79 | "roots": [ 80 | "/libs/", 81 | "/apps/" 82 | ], 83 | "moduleNameMapper": { 84 | "^@shared/@shared(|/.*)$": "/libs/@shared/src/$1" 85 | } 86 | }, 87 | "mikro-orm": { 88 | "useTsNode": true, 89 | "configPaths": [ 90 | "./mikro-orm.config.ts" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "esModuleInterop": true, 21 | "paths": { 22 | "@shared": [ 23 | "libs/@shared/src" 24 | ], 25 | "@shared/*": [ 26 | "libs/@shared/src/*" 27 | ] 28 | } 29 | } 30 | } --------------------------------------------------------------------------------