├── .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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 | }
--------------------------------------------------------------------------------