@rolandsall24/nest-mediator 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +650 -1349
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/aggregate/aggregate-repository.base.d.ts +90 -0
  7. package/dist/lib/aggregate/aggregate-repository.base.d.ts.map +1 -0
  8. package/dist/lib/aggregate/aggregate-repository.base.js +161 -0
  9. package/dist/lib/aggregate/aggregate-repository.base.js.map +1 -0
  10. package/dist/lib/aggregate/aggregate-root.base.d.ts +105 -0
  11. package/dist/lib/aggregate/aggregate-root.base.d.ts.map +1 -0
  12. package/dist/lib/aggregate/aggregate-root.base.js +127 -0
  13. package/dist/lib/aggregate/aggregate-root.base.js.map +1 -0
  14. package/dist/lib/aggregate/index.d.ts +3 -0
  15. package/dist/lib/aggregate/index.d.ts.map +1 -0
  16. package/dist/lib/aggregate/index.js +19 -0
  17. package/dist/lib/aggregate/index.js.map +1 -0
  18. package/dist/lib/context/index.d.ts +2 -0
  19. package/dist/lib/context/index.d.ts.map +1 -0
  20. package/dist/lib/context/index.js +18 -0
  21. package/dist/lib/context/index.js.map +1 -0
  22. package/dist/lib/context/mediator-context.d.ts +78 -0
  23. package/dist/lib/context/mediator-context.d.ts.map +1 -0
  24. package/dist/lib/context/mediator-context.js +93 -0
  25. package/dist/lib/context/mediator-context.js.map +1 -0
  26. package/dist/lib/decorators/domain-event.decorator.d.ts +48 -0
  27. package/dist/lib/decorators/domain-event.decorator.d.ts.map +1 -0
  28. package/dist/lib/decorators/domain-event.decorator.js +60 -0
  29. package/dist/lib/decorators/domain-event.decorator.js.map +1 -0
  30. package/dist/lib/decorators/for-aggregate.decorator.d.ts +21 -0
  31. package/dist/lib/decorators/for-aggregate.decorator.d.ts.map +1 -0
  32. package/dist/lib/decorators/for-aggregate.decorator.js +29 -0
  33. package/dist/lib/decorators/for-aggregate.decorator.js.map +1 -0
  34. package/dist/lib/decorators/index.d.ts +2 -0
  35. package/dist/lib/decorators/index.d.ts.map +1 -1
  36. package/dist/lib/decorators/index.js +2 -0
  37. package/dist/lib/decorators/index.js.map +1 -1
  38. package/dist/lib/event-store/aggregate-info.extractor.d.ts +35 -0
  39. package/dist/lib/event-store/aggregate-info.extractor.d.ts.map +1 -0
  40. package/dist/lib/event-store/aggregate-info.extractor.js +41 -0
  41. package/dist/lib/event-store/aggregate-info.extractor.js.map +1 -0
  42. package/dist/lib/event-store/event-store-persistence.consumer.d.ts +33 -0
  43. package/dist/lib/event-store/event-store-persistence.consumer.d.ts.map +1 -0
  44. package/dist/lib/event-store/event-store-persistence.consumer.js +79 -0
  45. package/dist/lib/event-store/event-store-persistence.consumer.js.map +1 -0
  46. package/dist/lib/event-store/index.d.ts +6 -0
  47. package/dist/lib/event-store/index.d.ts.map +1 -0
  48. package/dist/lib/event-store/index.js +22 -0
  49. package/dist/lib/event-store/index.js.map +1 -0
  50. package/dist/lib/event-store/repositories/postgres-event-store.repository.d.ts +49 -0
  51. package/dist/lib/event-store/repositories/postgres-event-store.repository.d.ts.map +1 -0
  52. package/dist/lib/event-store/repositories/postgres-event-store.repository.js +145 -0
  53. package/dist/lib/event-store/repositories/postgres-event-store.repository.js.map +1 -0
  54. package/dist/lib/event-store/schema/postgres.schema.d.ts +15 -0
  55. package/dist/lib/event-store/schema/postgres.schema.d.ts.map +1 -0
  56. package/dist/lib/event-store/schema/postgres.schema.js +68 -0
  57. package/dist/lib/event-store/schema/postgres.schema.js.map +1 -0
  58. package/dist/lib/event-store/strategies/abstract-postgres-connection.strategy.d.ts +21 -0
  59. package/dist/lib/event-store/strategies/abstract-postgres-connection.strategy.d.ts.map +1 -0
  60. package/dist/lib/event-store/strategies/abstract-postgres-connection.strategy.js +36 -0
  61. package/dist/lib/event-store/strategies/abstract-postgres-connection.strategy.js.map +1 -0
  62. package/dist/lib/event-store/strategies/custom-repository.strategy.d.ts +20 -0
  63. package/dist/lib/event-store/strategies/custom-repository.strategy.d.ts.map +1 -0
  64. package/dist/lib/event-store/strategies/custom-repository.strategy.js +38 -0
  65. package/dist/lib/event-store/strategies/custom-repository.strategy.js.map +1 -0
  66. package/dist/lib/event-store/strategies/event-store-strategy.interface.d.ts +10 -0
  67. package/dist/lib/event-store/strategies/event-store-strategy.interface.d.ts.map +1 -0
  68. package/dist/lib/event-store/strategies/event-store-strategy.interface.js +3 -0
  69. package/dist/lib/event-store/strategies/event-store-strategy.interface.js.map +1 -0
  70. package/dist/lib/event-store/strategies/existing-pool.strategy.d.ts +14 -0
  71. package/dist/lib/event-store/strategies/existing-pool.strategy.d.ts.map +1 -0
  72. package/dist/lib/event-store/strategies/existing-pool.strategy.js +24 -0
  73. package/dist/lib/event-store/strategies/existing-pool.strategy.js.map +1 -0
  74. package/dist/lib/event-store/strategies/index.d.ts +17 -0
  75. package/dist/lib/event-store/strategies/index.d.ts.map +1 -0
  76. package/dist/lib/event-store/strategies/index.js +50 -0
  77. package/dist/lib/event-store/strategies/index.js.map +1 -0
  78. package/dist/lib/event-store/strategies/persistence-consumer.strategy.d.ts +11 -0
  79. package/dist/lib/event-store/strategies/persistence-consumer.strategy.d.ts.map +1 -0
  80. package/dist/lib/event-store/strategies/persistence-consumer.strategy.js +32 -0
  81. package/dist/lib/event-store/strategies/persistence-consumer.strategy.js.map +1 -0
  82. package/dist/lib/event-store/strategies/postgres-schema-manager.d.ts +11 -0
  83. package/dist/lib/event-store/strategies/postgres-schema-manager.d.ts.map +1 -0
  84. package/dist/lib/event-store/strategies/postgres-schema-manager.js +25 -0
  85. package/dist/lib/event-store/strategies/postgres-schema-manager.js.map +1 -0
  86. package/dist/lib/event-store/strategies/schema-manager.interface.d.ts +10 -0
  87. package/dist/lib/event-store/strategies/schema-manager.interface.d.ts.map +1 -0
  88. package/dist/lib/{interfaces/event-handler.interface.js → event-store/strategies/schema-manager.interface.js} +1 -1
  89. package/dist/lib/event-store/strategies/schema-manager.interface.js.map +1 -0
  90. package/dist/lib/event-store/strategies/url-connection.strategy.d.ts +14 -0
  91. package/dist/lib/event-store/strategies/url-connection.strategy.d.ts.map +1 -0
  92. package/dist/lib/event-store/strategies/url-connection.strategy.js +26 -0
  93. package/dist/lib/event-store/strategies/url-connection.strategy.js.map +1 -0
  94. package/dist/lib/interfaces/event-consumer.interface.d.ts +90 -11
  95. package/dist/lib/interfaces/event-consumer.interface.d.ts.map +1 -1
  96. package/dist/lib/interfaces/event-store.interface.d.ts +157 -0
  97. package/dist/lib/interfaces/event-store.interface.d.ts.map +1 -0
  98. package/dist/lib/interfaces/event-store.interface.js +23 -0
  99. package/dist/lib/interfaces/event-store.interface.js.map +1 -0
  100. package/dist/lib/interfaces/index.d.ts +1 -0
  101. package/dist/lib/interfaces/index.d.ts.map +1 -1
  102. package/dist/lib/interfaces/index.js +1 -0
  103. package/dist/lib/interfaces/index.js.map +1 -1
  104. package/dist/lib/nest-mediator.module.d.ts +20 -0
  105. package/dist/lib/nest-mediator.module.d.ts.map +1 -1
  106. package/dist/lib/nest-mediator.module.js +54 -6
  107. package/dist/lib/nest-mediator.module.js.map +1 -1
  108. package/dist/lib/services/event.bus.d.ts +35 -7
  109. package/dist/lib/services/event.bus.d.ts.map +1 -1
  110. package/dist/lib/services/event.bus.js +133 -56
  111. package/dist/lib/services/event.bus.js.map +1 -1
  112. package/dist/lib/services/mediator.bus.d.ts +13 -4
  113. package/dist/lib/services/mediator.bus.d.ts.map +1 -1
  114. package/dist/lib/services/mediator.bus.js +35 -5
  115. package/dist/lib/services/mediator.bus.js.map +1 -1
  116. package/package.json +44 -7
  117. package/dist/lib/interfaces/event-handler.interface.d.ts +0 -24
  118. package/dist/lib/interfaces/event-handler.interface.d.ts.map +0 -1
  119. package/dist/lib/interfaces/event-handler.interface.js.map +0 -1
  120. package/dist/tsconfig.lib.tsbuildinfo +0 -1
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # NestJS Mediator
2
2
 
3
- A lightweight CQRS (Command Query Responsibility Segregation) mediator pattern implementation for NestJS applications.
3
+ A lightweight CQRS mediator for NestJS start simple, add event persistence when you need it, scale to full event sourcing when you're ready.
4
4
 
5
5
  ## Features
6
6
 
7
- - Clean separation between Commands, Queries, and Events
8
- - Type-safe handlers with TypeScript
9
- - Decorator-based handler registration
10
- - Automatic handler discovery and registration
11
- - Pipeline Behaviors for cross-cutting concerns (logging, validation, etc.)
12
- - Type-Specific Behaviors - behaviors that only apply to specific request types (v0.6.0+)
13
- - **Domain Events with Critical/Non-Critical consumers (v0.7.0+)**
14
- - Built-in behaviors: Logging, Validation, Exception Handling, Performance Tracking
15
- - Built on top of NestJS dependency injection
16
- - Zero runtime dependencies beyond NestJS
7
+ - **CQRS** Commands, Queries, and Domain Events with type-safe handlers
8
+ - **Three architecture modes** — Simple (no DB), Audit (event log), Source (event sourcing)
9
+ - **Zero-boilerplate event sourcing** — `@ForAggregate` + `@DomainEvent` eliminate all wiring
10
+ - **Critical & Non-Critical consumers** — Sequential saga-style or fire-and-forget
11
+ - **Saga compensation** Automatic rollback via `applyCompensatingEvent()`
12
+ - **Pipeline Behaviors** — Cross-cutting concerns (logging, validation, caching, retry)
13
+ - **Flexible Event Store** — PostgreSQL-backed, bring your own pool or repository
14
+ - **Optimistic Concurrency** Sequence-based version control with `ConcurrencyError`
15
+ - **Correlation & Causation IDs** Automatic distributed tracing via `AsyncLocalStorage`
16
+ - **Zero config** Decorator-based auto-discovery, built on NestJS DI
17
17
 
18
18
  ## Installation
19
19
 
@@ -21,9 +21,7 @@ A lightweight CQRS (Command Query Responsibility Segregation) mediator pattern i
21
21
  npm install @rolandsall24/nest-mediator
22
22
  ```
23
23
 
24
- ### TypeScript Configuration
25
-
26
- This library requires TypeScript decorators to be enabled. Add the following to your `tsconfig.json`:
24
+ **TypeScript configuration** — enable decorators in `tsconfig.json`:
27
25
 
28
26
  ```json
29
27
  {
@@ -34,296 +32,303 @@ This library requires TypeScript decorators to be enabled. Add the following to
34
32
  }
35
33
  ```
36
34
 
37
- ## Upgrading to v0.7.0
38
-
39
- Version 0.7.0 introduces **Domain Events** with Critical and Non-Critical consumer support.
40
-
41
- ### What's New
42
-
43
- - `IEvent` interface for domain events
44
- - `IEventConsumer<TEvent>` interface for event consumers
45
- - `ICriticalEventConsumer<TEvent>` interface for critical consumers with optional compensation
46
- - `@EventHandler(EventClass)` decorator to register consumers
47
- - `@Critical({ order: n })` decorator for critical consumers (run sequentially)
48
- - `@NonCritical()` decorator for non-critical consumers (fire-and-forget)
49
- - `mediatorBus.publish(event)` method to publish events
50
- - `EventCriticality` enum (`CRITICAL`, `NON_CRITICAL`)
51
- - **Saga-style compensation**: Critical consumers can define a `compensate()` method that runs in reverse order when a subsequent consumer fails
52
- - Internal architecture refactoring (MediatorBus now delegates to CommandBus, QueryBus, EventBus)
53
-
54
- ### Backward Compatibility
55
-
56
- - **`IEventHandler` renamed to `IEventConsumer`**: `IEventHandler` is now a deprecated type alias. Update your imports:
57
-
58
- - **MediatorBus API unchanged**: The `send()`, `query()`, and `publish()` methods work exactly as before
59
- - **No breaking changes**: Existing command/query code continues to work without modifications
60
-
61
- ---
62
-
63
- ## Upgrading to v0.6.0
64
-
65
- Version 0.6.0 introduces **Type-Specific Pipeline Behaviors** - behaviors that only apply to specific request types.
66
-
67
- ### What's New
35
+ ## Choose Your Architecture
68
36
 
69
- - New `@Handle()` decorator for the `handle` method to enable automatic request type inference
70
- - Behaviors can target specific command/query types without manual `instanceof` checks
71
- - Full backward compatibility - existing behaviors work unchanged
37
+ NestJS Mediator grows with your application. Start simple, add what you need.
72
38
 
73
- ### Type-Specific Behavior Example
39
+ | | **Simple** | **Audit** | **Source** |
40
+ |---|---|---|---|
41
+ | **Database required** | No | PostgreSQL | PostgreSQL |
42
+ | **State storage** | Your choice (in-memory, any DB) | Your tables (e.g., `orders`) | Event store only |
43
+ | **Event persistence** | None | Events logged alongside state | Events ARE the state |
44
+ | **Aggregates** | Not needed | Not needed | `AggregateRoot` + `AggregateRepository` |
45
+ | **Concurrency control** | Your responsibility | Your responsibility | Built-in (optimistic) |
46
+ | **Use when** | Prototyping, simple apps | Production apps needing audit trail | Domain-driven, event-sourced systems |
74
47
 
75
- ```typescript
76
- import { Injectable } from '@nestjs/common';
77
- import { IPipelineBehavior, PipelineBehavior, Handle } from '@rolandsall24/nest-mediator';
78
- import { CreateUserCommand } from './create-user.command';
79
-
80
- @Injectable()
81
- @PipelineBehavior({ priority: 100, scope: 'command' })
82
- export class CreateUserValidationBehavior
83
- implements IPipelineBehavior<CreateUserCommand, void>
84
- {
85
- @Handle() // <-- Enables type inference from method signature
86
- async handle(
87
- request: CreateUserCommand,
88
- next: () => Promise<void>,
89
- ): Promise<void> {
90
- // This behavior ONLY runs for CreateUserCommand
91
- // No instanceof check needed!
92
- if (!request.email.includes('@')) {
93
- throw new Error('Invalid email');
94
- }
95
- return next();
96
- }
97
- }
98
48
  ```
99
-
100
- ### How It Works
101
-
102
- 1. Apply `@PipelineBehavior()` to the class and `@Handle()` to the `handle` method
103
- 2. TypeScript's `emitDecoratorMetadata` emits type information for the method parameters
104
- 3. The library reads this metadata at registration time and filters behaviors by request type
105
-
106
- ### No Migration Required
107
-
108
- Existing behaviors without `@Handle()` on the method continue to work exactly as before - they apply to all requests matching their scope.
49
+ Simple ──────────► Audit ──────────► Source
50
+ No DB + event log + event sourcing
51
+ Just CQRS + traceability + aggregates
52
+ + audit trail + concurrency control
53
+ ```
109
54
 
110
55
  ---
111
56
 
112
- ## Quick Start
57
+ ## Mode 1: Simple (No Database Required)
113
58
 
114
- ### 1. Import the Module
59
+ Pure CQRS with commands, queries, and domain events. No event store, no database — just clean separation of concerns.
115
60
 
116
- Import `NestMediatorModule` in your application module:
61
+ ### Module Setup
117
62
 
118
63
  ```typescript
119
64
  import { Module } from '@nestjs/common';
120
65
  import { NestMediatorModule } from '@rolandsall24/nest-mediator';
121
- import { CreateUserCommandHandler } from './handlers/create-user.handler';
122
- import { GetUserQueryHandler } from './handlers/get-user-query.handler';
123
-
124
- @Module({
125
- imports: [
126
- // Basic setup
127
- NestMediatorModule.forRoot(),
128
- ],
129
- providers: [
130
- // Add your handlers to the providers array
131
- // They will be automatically discovered by the mediator
132
- CreateUserCommandHandler,
133
- GetUserQueryHandler,
134
- ],
135
- })
136
- export class AppModule {}
137
- ```
138
-
139
- Or with built-in pipeline behaviors enabled:
140
66
 
141
- ```typescript
142
67
  @Module({
143
68
  imports: [
144
69
  NestMediatorModule.forRoot({
145
- enableLogging: true, // Log request handling with timing
146
- enableValidation: true, // Validate requests with class-validator
147
- enableExceptionHandling: true, // Centralized exception logging
70
+ enableLogging: true,
71
+ enableValidation: true,
148
72
  }),
149
73
  ],
150
74
  providers: [
151
- CreateUserCommandHandler,
152
- GetUserQueryHandler,
75
+ CreateUserHandler,
76
+ GetUserHandler,
77
+ SendWelcomeEmailConsumer,
153
78
  ],
154
79
  })
155
80
  export class AppModule {}
156
81
  ```
157
82
 
158
- **How it works**: The module uses NestJS's `DiscoveryService` to automatically discover and register all providers decorated with `@CommandHandler`, `@QueryHandler`, or `@PipelineBehavior`. Simply add your handlers to the module's `providers` array and they will be automatically registered with the mediator!
159
-
160
- ## Usage
161
-
162
- ### Commands
163
-
164
- Commands are used for operations that change state (create, update, delete).
165
-
166
- #### 1. Define a Command
83
+ ### Define Commands & Handlers
167
84
 
168
85
  ```typescript
169
- import { ICommand } from '@rolandsall24/nest-mediator';
86
+ import { ICommand, ICommandHandler, CommandHandler, MediatorBus } from '@rolandsall24/nest-mediator';
170
87
 
88
+ // Command — a simple data container
171
89
  export class CreateUserCommand implements ICommand {
172
90
  constructor(
173
91
  public readonly email: string,
174
92
  public readonly name: string,
175
- public readonly age: number
176
93
  ) {}
177
94
  }
95
+
96
+ // Handler — contains the business logic
97
+ @Injectable()
98
+ @CommandHandler(CreateUserCommand)
99
+ export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
100
+ constructor(private readonly mediatorBus: MediatorBus) {}
101
+
102
+ async execute(command: CreateUserCommand): Promise<void> {
103
+ const userId = randomUUID();
104
+ // Save user to your database...
105
+
106
+ // Publish domain event to trigger side effects
107
+ await this.mediatorBus.publish(
108
+ new UserCreatedEvent(userId, command.email, command.name),
109
+ );
110
+ }
111
+ }
178
112
  ```
179
113
 
180
- #### 2. Create a Command Handler
114
+ ### Define Queries & Handlers
181
115
 
182
116
  ```typescript
183
- import { Injectable } from '@nestjs/common';
184
- import { CommandHandler, ICommandHandler } from '@rolandsall24/nest-mediator';
185
- import { CreateUserCommand } from '../commands/create-user.command';
117
+ import { IQuery, IQueryHandler, QueryHandler } from '@rolandsall24/nest-mediator';
118
+
119
+ export class GetUserQuery implements IQuery {
120
+ constructor(public readonly userId: string) {}
121
+ }
186
122
 
187
123
  @Injectable()
188
- @CommandHandler(CreateUserCommand)
189
- export class CreateUserCommandHandler implements ICommandHandler<CreateUserCommand> {
124
+ @QueryHandler(GetUserQuery)
125
+ export class GetUserHandler implements IQueryHandler<GetUserQuery, UserDto> {
126
+ async execute(query: GetUserQuery): Promise<UserDto> {
127
+ // Fetch and return user...
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### Define Events & Consumers
133
+
134
+ ```typescript
135
+ import { IEvent, IEventConsumer, EventHandler, NonCritical } from '@rolandsall24/nest-mediator';
136
+
137
+ export class UserCreatedEvent implements IEvent {
190
138
  constructor(
191
- // Inject your services here
192
- // private readonly userRepository: UserRepository,
139
+ public readonly userId: string,
140
+ public readonly email: string,
141
+ public readonly name: string,
193
142
  ) {}
143
+ }
194
144
 
195
- async execute(command: CreateUserCommand): Promise<void> {
196
- // Business logic here
197
- console.log(`Creating user: ${command.email}`);
198
-
199
- // Example: Save to database
200
- // await this.userRepository.save({
201
- // email: command.email,
202
- // name: command.name,
203
- // age: command.age,
204
- // });
145
+ @Injectable()
146
+ @EventHandler(UserCreatedEvent)
147
+ @NonCritical()
148
+ export class SendWelcomeEmailConsumer implements IEventConsumer<UserCreatedEvent> {
149
+ async handle(event: UserCreatedEvent): Promise<void> {
150
+ // Send welcome email — fire-and-forget
205
151
  }
206
152
  }
207
153
  ```
208
154
 
209
- #### 3. Send a Command from Controller
155
+ ### Use from Controllers
210
156
 
211
157
  ```typescript
212
- import { Controller, Post, Body } from '@nestjs/common';
213
- import { MediatorBus } from '@rolandsall24/nest-mediator';
214
- import { CreateUserCommand } from './commands/create-user.command';
215
-
216
158
  @Controller('users')
217
159
  export class UserController {
218
160
  constructor(private readonly mediator: MediatorBus) {}
219
161
 
220
162
  @Post()
221
- async create(@Body() body: { email: string; name: string; age: number }): Promise<void> {
222
- const command = new CreateUserCommand(
223
- body.email,
224
- body.name,
225
- body.age
226
- );
163
+ async create(@Body() body: CreateUserDto) {
164
+ await this.mediator.send(new CreateUserCommand(body.email, body.name));
165
+ }
227
166
 
228
- await this.mediator.send(command);
167
+ @Get(':id')
168
+ async getById(@Param('id') id: string) {
169
+ return this.mediator.query(new GetUserQuery(id));
229
170
  }
230
171
  }
231
172
  ```
232
173
 
233
- ### Queries
234
-
235
- Queries are used for operations that read data without changing state.
174
+ ---
236
175
 
237
- #### 1. Define a Query
176
+ ## Mode 2: Audit (Event Logging)
238
177
 
239
- ```typescript
240
- import { IQuery } from '@rolandsall24/nest-mediator';
178
+ Everything from Simple mode, plus **every domain event is automatically persisted** to a PostgreSQL table. Your application still manages state in its own tables — the event log provides traceability, debugging, and compliance.
241
179
 
242
- export class GetUserByIdQuery implements IQuery {
243
- constructor(public readonly userId: string) {}
244
- }
245
- ```
180
+ ### What changes from Simple mode
246
181
 
247
- #### 2. Define a Query Result Type
182
+ 1. Add `eventStore` config with `mode: 'audit'`
183
+ 2. That's it — events are persisted automatically
248
184
 
249
185
  ```typescript
250
- export interface UserDto {
251
- id: string;
252
- email: string;
253
- name: string;
254
- age: number;
255
- createdAt: Date;
256
- }
186
+ @Module({
187
+ imports: [
188
+ NestMediatorModule.forRoot({
189
+ enableLogging: true,
190
+ eventStore: {
191
+ type: 'postgres',
192
+ url: process.env.DATABASE_URL,
193
+ mode: 'audit',
194
+ tableName: 'audit_events', // optional, defaults to 'domain_events'
195
+ },
196
+ }),
197
+ ],
198
+ providers: [
199
+ // Your application manages state in its own tables
200
+ { provide: ORDER_PERSISTOR, useClass: PostgresOrderAdapter },
201
+
202
+ CreateOrderHandler,
203
+ GetOrderHandler,
204
+ ReserveInventoryHandler,
205
+ ProcessPaymentHandler,
206
+ ],
207
+ })
208
+ export class AppModule {}
257
209
  ```
258
210
 
259
- #### 3. Create a Query Handler
211
+ ### How it works
260
212
 
261
- ```typescript
262
- import { Injectable } from '@nestjs/common';
263
- import { QueryHandler, IQueryHandler } from '@rolandsall24/nest-mediator';
264
- import { GetUserByIdQuery } from '../queries/get-user-by-id.query';
265
- import { UserDto } from '../dtos/user.dto';
213
+ Your command handler saves state to your own table, then publishes a domain event. The library automatically persists the event to the audit table before dispatching to consumers.
266
214
 
215
+ ```typescript
267
216
  @Injectable()
268
- @QueryHandler(GetUserByIdQuery)
269
- export class GetUserByIdQueryHandler implements IQueryHandler<GetUserByIdQuery, UserDto> {
217
+ @CommandHandler(CreateOrderCommand)
218
+ export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
270
219
  constructor(
271
- // Inject your services here
272
- // private readonly userRepository: UserRepository,
220
+ @Inject(ORDER_PERSISTOR) private readonly orderPersistor: IOrderPersistor,
221
+ private readonly mediatorBus: MediatorBus,
273
222
  ) {}
274
223
 
275
- async execute(query: GetUserByIdQuery): Promise<UserDto> {
276
- // Business logic here
277
- console.log(`Fetching user with ID: ${query.userId}`);
278
-
279
- // Example: Fetch from database
280
- // const user = await this.userRepository.findById(query.userId);
281
-
282
- // Return mock data for demonstration
283
- return {
284
- id: query.userId,
285
- email: 'john.doe@example.com',
286
- name: 'John Doe',
287
- age: 30,
288
- createdAt: new Date(),
289
- };
224
+ async execute(command: CreateOrderCommand): Promise<void> {
225
+ const orderId = `order-${Date.now()}`;
226
+
227
+ // 1. Save to YOUR orders table (source of truth)
228
+ await this.orderPersistor.save({
229
+ orderId,
230
+ customerId: command.customerId,
231
+ items: command.items,
232
+ total: command.total,
233
+ status: 'placed',
234
+ });
235
+
236
+ // 2. Publish event — automatically logged to audit_events table
237
+ await this.mediatorBus.publish(
238
+ new OrderPlacedEvent(orderId, command.customerId, command.items, command.total),
239
+ );
290
240
  }
291
241
  }
292
242
  ```
293
243
 
294
- #### 4. Execute a Query from Controller
244
+ ### Optional: `@DomainEvent` for richer audit logs
295
245
 
296
- ```typescript
297
- import { Controller, Get, Param } from '@nestjs/common';
298
- import { MediatorBus } from '@rolandsall24/nest-mediator';
299
- import { GetUserByIdQuery } from './queries/get-user-by-id.query';
300
- import { UserDto } from './dtos/user.dto';
246
+ In audit mode, `@DomainEvent` is **purely informational** — it does not change runtime behavior. Adding it populates the `aggregate_type` and `aggregate_id` columns in the event store, which makes querying and filtering your audit log significantly easier.
301
247
 
302
- @Controller('users')
303
- export class UserController {
304
- constructor(private readonly mediator: MediatorBus) {}
248
+ ```typescript
249
+ // Without @DomainEvent — events are still persisted, but aggregate_type and aggregate_id are null
250
+ export class OrderPlacedEvent implements IEvent {
251
+ constructor(public readonly orderId: string, public readonly total: number) {}
252
+ }
305
253
 
306
- @Get(':id')
307
- async getById(@Param('id') id: string): Promise<UserDto> {
308
- const query = new GetUserByIdQuery(id);
309
- const user = await this.mediator.query<GetUserByIdQuery, UserDto>(query);
310
- return user;
311
- }
254
+ // With @DomainEvent — aggregate_type='Order', aggregate_id=orderId in the audit table
255
+ @DomainEvent('Order', 'orderId')
256
+ export class OrderPlacedEvent implements IEvent {
257
+ constructor(public readonly orderId: string, public readonly total: number) {}
312
258
  }
313
259
  ```
314
260
 
315
- ### Domain Events
261
+ This is optional. Events are logged either way. The decorator just gives you better data for queries like *"show me all events for order X"* or *"show me all Order events"*.
262
+
263
+ ### What you get
264
+
265
+ Every event is stored with full context:
266
+
267
+ | Column | Description |
268
+ |--------|-------------|
269
+ | `event_id` | Unique event identifier |
270
+ | `event_type` | Class name (e.g., `OrderPlacedEvent`) |
271
+ | `aggregate_type` | Aggregate name from `@DomainEvent` (if present) |
272
+ | `aggregate_id` | Aggregate ID from `@DomainEvent` (if present) |
273
+ | `correlation_id` | Groups all events from the same business transaction |
274
+ | `causation_id` | Points to the parent event that caused this one |
275
+ | `payload` | Full event data as JSON |
276
+ | `occurred_at` | Timestamp |
277
+
278
+ ### When to use Audit mode
279
+
280
+ - You need an **audit trail** for compliance or debugging
281
+ - You want to **trace event chains** (correlation/causation IDs)
282
+ - You're happy managing state in your own tables
283
+ - You want to **answer "what happened?"** without changing your architecture
284
+
285
+ ---
286
+
287
+ ## Mode 3: Source (Event Sourcing)
288
+
289
+ Events **are** the data. There are no state tables. Application state is rebuilt by replaying events from the event store.
290
+
291
+ ### What changes from Audit mode
292
+
293
+ 1. Change `mode: 'audit'` to `mode: 'source'`
294
+ 2. Define an `AggregateRoot` with business rules
295
+ 3. Add an `AggregateRepository` with `@ForAggregate` (one line)
296
+ 4. Gain **optimistic concurrency control** for free
297
+
298
+ ```typescript
299
+ @Module({
300
+ imports: [
301
+ NestMediatorModule.forRoot({
302
+ enableLogging: true,
303
+ eventStore: {
304
+ type: 'postgres',
305
+ url: process.env.DATABASE_URL,
306
+ mode: 'source',
307
+ tableName: 'domain_events',
308
+ },
309
+ }),
310
+ ],
311
+ providers: [
312
+ OrderAggregateRepository,
316
313
 
317
- Domain events notify other parts of the system when something important happens. They support two consumer types:
314
+ PlaceOrderHandler,
315
+ CancelOrderHandler,
316
+ GetOrderHandler,
317
+ ReserveInventoryHandler,
318
+ ProcessPaymentHandler,
319
+ ],
320
+ })
321
+ export class AppModule {}
322
+ ```
318
323
 
319
- - **Critical consumers**: Run sequentially in order. Must succeed for the operation to complete.
320
- - **Non-critical consumers**: Run in parallel after critical consumers complete. Fire-and-forget.
324
+ ### Mark Events with `@DomainEvent`
321
325
 
322
- #### 1. Define an Event
326
+ The `@DomainEvent` decorator associates events with aggregates and registers them in a global event registry. **Required** for source mode (used for aggregate hydration). **Optional** for audit mode (only populates `aggregate_type`/`aggregate_id` columns for easier querying — has no effect on runtime behavior).
323
327
 
324
328
  ```typescript
325
- import { IEvent } from '@rolandsall24/nest-mediator';
329
+ import { IEvent, DomainEvent } from '@rolandsall24/nest-mediator';
326
330
 
331
+ @DomainEvent('Order', 'orderId')
327
332
  export class OrderPlacedEvent implements IEvent {
328
333
  constructor(
329
334
  public readonly orderId: string,
@@ -332,1350 +337,646 @@ export class OrderPlacedEvent implements IEvent {
332
337
  public readonly total: number,
333
338
  ) {}
334
339
  }
340
+
341
+ @DomainEvent('Order', 'orderId')
342
+ export class OrderCancelledEvent implements IEvent {
343
+ constructor(
344
+ public readonly orderId: string,
345
+ public readonly reason: string,
346
+ ) {}
347
+ }
335
348
  ```
336
349
 
337
- #### 2. Create Event Consumers
350
+ The first argument is the aggregate type name (must match `aggregateType` on your aggregate). The second is the property name holding the aggregate ID. The `@DomainEvent` decorator also auto-registers the event class so `AggregateRepository` can deserialize stored events without any manual wiring.
351
+
352
+ ### Define an Aggregate
338
353
 
339
- **Critical consumer** (must succeed, runs in order):
354
+ The aggregate encapsulates business rules and tracks state changes as events.
340
355
 
341
356
  ```typescript
342
- import { Injectable, Logger } from '@nestjs/common';
343
- import { EventHandler, IEventConsumer, Critical } from '@rolandsall24/nest-mediator';
344
- import { OrderPlacedEvent } from './order-placed.event';
357
+ import { AggregateRoot } from '@rolandsall24/nest-mediator';
345
358
 
346
- @Injectable()
347
- @EventHandler(OrderPlacedEvent)
348
- @Critical({ order: 1 }) // Runs first among critical consumers
349
- export class ValidateInventoryConsumer implements IEventConsumer<OrderPlacedEvent> {
350
- private readonly logger = new Logger(ValidateInventoryConsumer.name);
359
+ export class OrderAggregate extends AggregateRoot<string> {
360
+ private _orderId!: string;
361
+ private _customerId!: string;
362
+ private _items: { productId: string; quantity: number }[] = [];
363
+ private _total: number = 0;
364
+ private _status: 'placed' | 'cancelled' = 'placed';
351
365
 
352
- async handle(event: OrderPlacedEvent): Promise<void> {
353
- this.logger.log(`Validating inventory for order ${event.orderId}`);
354
- // Validate all items are in stock
355
- // Throw error if validation fails - stops the event processing
356
- }
357
- }
358
- ```
366
+ readonly aggregateType = 'Order';
367
+ get id() { return this._orderId; }
368
+ get status() { return this._status; }
359
369
 
360
- **Critical consumer with compensation** (implements rollback on failure):
370
+ // ── Command methods (enforce business rules, emit events) ──
361
371
 
362
- ```typescript
363
- import { Injectable, Logger } from '@nestjs/common';
364
- import { EventHandler, ICriticalEventConsumer, Critical } from '@rolandsall24/nest-mediator';
365
- import { OrderPlacedEvent } from './order-placed.event';
372
+ static create(
373
+ orderId: string,
374
+ customerId: string,
375
+ items: { productId: string; quantity: number }[],
376
+ total: number,
377
+ ): OrderAggregate {
378
+ if (!items.length) throw new Error('Order must have at least one item');
379
+ const order = new OrderAggregate();
380
+ order.apply(new OrderPlacedEvent(orderId, customerId, items, total));
381
+ return order;
382
+ }
366
383
 
367
- @Injectable()
368
- @EventHandler(OrderPlacedEvent)
369
- @Critical({ order: 2 })
370
- export class ReserveInventoryConsumer implements ICriticalEventConsumer<OrderPlacedEvent> {
371
- private readonly logger = new Logger(ReserveInventoryConsumer.name);
384
+ cancel(reason: string): void {
385
+ if (this._status === 'cancelled') {
386
+ throw new Error(`Order ${this._orderId} is already cancelled`);
387
+ }
388
+ this.apply(new OrderCancelledEvent(this._orderId, reason));
389
+ }
372
390
 
373
- async handle(event: OrderPlacedEvent): Promise<void> {
374
- this.logger.log(`Reserving inventory for order ${event.orderId}`);
375
- // Reserve inventory in the database
391
+ // ── Event handlers (update state from events) ──
392
+
393
+ applyOrderPlacedEvent(event: OrderPlacedEvent): void {
394
+ this._orderId = event.orderId;
395
+ this._customerId = event.customerId;
396
+ this._items = event.items;
397
+ this._total = event.total;
398
+ this._status = 'placed';
376
399
  }
377
400
 
378
- // Called if a SUBSEQUENT critical consumer fails (e.g., ChargePayment at order 4)
379
- async compensate(event: OrderPlacedEvent): Promise<void> {
380
- this.logger.warn(`[COMPENSATE] Releasing inventory for order ${event.orderId}`);
381
- // Release the reserved inventory
401
+ applyOrderCancelledEvent(event: OrderCancelledEvent): void {
402
+ this._status = 'cancelled';
382
403
  }
383
404
  }
384
405
  ```
385
406
 
386
- **Non-critical consumer** (fire-and-forget):
407
+ #### The `applyXxxEvent` Convention
408
+
409
+ When you call `this.apply(new OrderPlacedEvent(...))`, the base class automatically looks for a method named `apply` + the event class name — in this case `applyOrderPlacedEvent`. This convention is used in two scenarios:
410
+
411
+ 1. **New events** — when a command method calls `this.apply(event)`, the handler updates state and the event is tracked as uncommitted.
412
+ 2. **Replayed events** — when loading from history via `loadFromHistory()`, the same handlers rebuild state from stored events.
413
+
414
+ This means your aggregate's state mutation logic is written once and works for both paths. If a handler is missing, the library logs a warning for new events and silently skips during replay.
415
+
416
+ ### Define an Aggregate Repository
417
+
418
+ With `@ForAggregate`, the repository is a one-liner. The decorator tells the base class which aggregate to instantiate, and the `@DomainEvent` registry handles event deserialization automatically.
387
419
 
388
420
  ```typescript
389
- import { Injectable, Logger } from '@nestjs/common';
390
- import { EventHandler, IEventConsumer, NonCritical } from '@rolandsall24/nest-mediator';
391
- import { OrderPlacedEvent } from './order-placed.event';
421
+ import { Injectable } from '@nestjs/common';
422
+ import { AggregateRepository, ForAggregate } from '@rolandsall24/nest-mediator';
392
423
 
393
424
  @Injectable()
394
- @EventHandler(OrderPlacedEvent)
395
- @NonCritical() // Runs in background after critical consumers
396
- export class SendOrderConfirmationConsumer implements IEventConsumer<OrderPlacedEvent> {
397
- private readonly logger = new Logger(SendOrderConfirmationConsumer.name);
425
+ @ForAggregate(OrderAggregate)
426
+ export class OrderAggregateRepository
427
+ extends AggregateRepository<OrderAggregate, string> {}
428
+ ```
398
429
 
399
- async handle(event: OrderPlacedEvent): Promise<void> {
400
- this.logger.log(`Sending confirmation email for order ${event.orderId}`);
401
- // Send email - failures are logged but don't affect the order
402
- }
403
- }
430
+ That's it. No constructor, no overrides. The base class provides:
431
+
432
+ - **`findById(id)`** loads events from the store, deserializes them via the `@DomainEvent` registry, and replays them to rebuild aggregate state
433
+ - **`getById(id)`** — same as `findById` but throws if the aggregate doesn't exist
434
+ - **`save(aggregate)`** — publishes each uncommitted event through the event bus (persistence + consumers)
435
+
436
+ Need custom logic? Every method is overridable:
404
437
 
405
- // Consumers without @Critical or @NonCritical default to non-critical
438
+ ```typescript
406
439
  @Injectable()
407
- @EventHandler(OrderPlacedEvent)
408
- export class TrackAnalyticsConsumer implements IEventConsumer<OrderPlacedEvent> {
409
- async handle(event: OrderPlacedEvent): Promise<void> {
410
- // Track analytics - non-critical by default
440
+ @ForAggregate(OrderAggregate)
441
+ export class OrderAggregateRepository
442
+ extends AggregateRepository<OrderAggregate, string>
443
+ {
444
+ // Override any method for custom behavior
445
+ protected deserializeEvent(eventType: string, payload: Record<string, unknown>): IEvent {
446
+ // Custom deserialization logic
411
447
  }
412
448
  }
413
449
  ```
414
450
 
415
- #### 3. Publish Events from Command Handlers
416
-
417
- The recommended pattern is to publish domain events from command handlers after the main operation succeeds:
451
+ ### Use from Handlers
418
452
 
419
453
  ```typescript
420
- import { Injectable, Logger } from '@nestjs/common';
421
- import { CommandHandler, ICommandHandler, MediatorBus } from '@rolandsall24/nest-mediator';
422
- import { PlaceOrderCommand } from './place-order.command';
423
- import { OrderPlacedEvent } from '../events/order-placed.event';
424
-
425
454
  @Injectable()
426
455
  @CommandHandler(PlaceOrderCommand)
427
456
  export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
428
- private readonly logger = new Logger(PlaceOrderHandler.name);
429
-
430
- constructor(private readonly mediatorBus: MediatorBus) {}
457
+ constructor(private readonly orderRepository: OrderAggregateRepository) {}
431
458
 
432
459
  async execute(command: PlaceOrderCommand): Promise<void> {
433
- const orderId = `order-${Date.now()}`;
434
-
435
- // 1. Process the order (validate, save to DB, etc.)
436
- this.logger.log(`Processing order ${orderId}`);
437
- await this.processOrder(orderId, command);
438
-
439
- // 2. Publish domain event after successful processing
440
- // Critical consumers run sequentially, non-critical run in background
441
- const result = await this.mediatorBus.publish(
442
- new OrderPlacedEvent(
443
- orderId,
444
- command.customerId,
445
- command.items,
446
- command.total,
447
- ),
460
+ const order = OrderAggregate.create(
461
+ `order-${Date.now()}`,
462
+ command.customerId,
463
+ command.items,
464
+ command.total,
448
465
  );
449
466
 
450
- this.logger.log(
451
- `Order ${orderId} completed. Critical: ${result.criticalSucceeded}, Non-critical dispatched: ${result.nonCriticalDispatched}`,
452
- );
467
+ // Save publishes the event -> persistence -> critical consumers -> non-critical
468
+ await this.orderRepository.save(order);
453
469
  }
470
+ }
454
471
 
455
- private async processOrder(orderId: string, command: PlaceOrderCommand): Promise<void> {
456
- // Order processing logic here
472
+ @Injectable()
473
+ @CommandHandler(CancelOrderCommand)
474
+ export class CancelOrderHandler implements ICommandHandler<CancelOrderCommand> {
475
+ constructor(private readonly orderRepository: OrderAggregateRepository) {}
476
+
477
+ async execute(command: CancelOrderCommand): Promise<void> {
478
+ // Load aggregate (replays events -> rebuilds state + version)
479
+ const order = await this.orderRepository.findById(command.orderId);
480
+ if (!order) throw new OrderNotFoundException(command.orderId);
481
+
482
+ // Apply business rules, record OrderCancelledEvent
483
+ order.cancel(command.reason);
484
+
485
+ // Save with concurrency check — throws ConcurrencyError if version conflict
486
+ await this.orderRepository.save(order);
457
487
  }
458
488
  }
459
489
  ```
460
490
 
461
- #### 4. Event Execution Flow
491
+ ### Optimistic Concurrency
462
492
 
463
- ```
464
- publish(OrderPlacedEvent)
465
-
466
- ├─► Critical Consumers (sequential, awaited)
467
- │ ├─► ValidateInventoryConsumer (order: 1) ✓
468
- │ ├─► ReserveInventoryConsumer (order: 2) ✓ [has compensate()]
469
- │ ├─► CreateOrderRecordConsumer (order: 3) ✓ [has compensate()]
470
- │ └─► ChargePaymentConsumer (order: 4) ✗ FAILS
471
-
472
- │ On failure → Run compensations in REVERSE order:
473
- │ ├─► CreateOrderRecordConsumer.compensate() - deletes order
474
- │ └─► ReserveInventoryConsumer.compensate() - releases inventory
475
- │ Then throw original error
476
-
477
- └─► Non-Critical Consumers (parallel, fire-and-forget)
478
- ├─► SendOrderConfirmationConsumer
479
- ├─► NotifyWarehouseConsumer
480
- └─► TrackAnalyticsConsumer
493
+ In source mode, each event stored for an aggregate carries a sequence number. When two requests load the same aggregate simultaneously, both see version N. The first to save writes version N+1 successfully. The second gets a `ConcurrencyError` because the version has advanced.
481
494
 
482
- Only runs if ALL critical consumers succeed
483
- Failures logged but don't affect the result
495
+ ```
496
+ Request A: load(order) -> version 3 -> cancel() -> save() -> writes seq 4 ok
497
+ Request B: load(order) -> version 3 -> cancel() -> save() -> expected 3, found 4 -> ConcurrencyError
484
498
  ```
485
499
 
486
- #### 5. Compensation Pattern (Saga)
500
+ The `example-source` project includes a `POST /orders/test-concurrency` endpoint that demonstrates this by placing an order and firing two cancel commands simultaneously.
487
501
 
488
- Critical consumers can implement the `ICriticalEventConsumer` interface with an optional `compensate()` method to support saga-style rollback:
502
+ ### When to use Source mode
489
503
 
490
- ```typescript
491
- import { ICriticalEventConsumer, EventHandler, Critical } from '@rolandsall24/nest-mediator';
504
+ - You need a **complete history** of every state change
505
+ - You want **optimistic concurrency** without manual locking
506
+ - Your domain is complex enough to benefit from **aggregates and business rules**
507
+ - You want to **rebuild state** at any point in time
508
+ - You're building a **domain-driven** system where events are first-class citizens
509
+
510
+ ---
511
+
512
+ ## Core Concepts
513
+
514
+ ### Domain Events
515
+
516
+ Domain events notify other parts of the system when something happens. Consumers come in two types:
517
+
518
+ **Critical consumers** — run sequentially in order. Must succeed. Support compensation.
492
519
 
520
+ ```typescript
493
521
  @Injectable()
494
522
  @EventHandler(OrderPlacedEvent)
495
- @Critical({ order: 3 })
496
- export class CreateOrderRecordConsumer implements ICriticalEventConsumer<OrderPlacedEvent> {
523
+ @Critical({ order: 1 })
524
+ export class ReserveInventoryHandler implements ICriticalEventConsumer<OrderPlacedEvent> {
497
525
  async handle(event: OrderPlacedEvent): Promise<void> {
498
- // Create order in database
499
- await this.orderRepository.create({
500
- id: event.orderId,
501
- customerId: event.customerId,
502
- items: event.items,
503
- total: event.total,
504
- });
526
+ // Reserve inventory must succeed before payment
505
527
  }
506
528
 
507
- async compensate(event: OrderPlacedEvent): Promise<void> {
508
- // Rollback: delete the order record
509
- await this.orderRepository.delete(event.orderId);
529
+ // Called if a SUBSEQUENT critical consumer fails
530
+ async applyCompensatingEvent(event: OrderPlacedEvent): Promise<IEvent> {
531
+ return new InventoryReleasedEvent(event.orderId);
510
532
  }
511
533
  }
512
534
  ```
513
535
 
514
- **Compensation rules:**
515
- - Only called when a **subsequent** critical consumer fails (not if this consumer fails)
516
- - Runs in **reverse order** (last succeeded → first succeeded)
517
- - Receives the same event instance passed to `handle()`
518
- - Should be **idempotent** - safe to run multiple times
519
- - Errors in compensations are logged but don't stop other compensations
520
- - Non-critical consumers don't need compensation (they're fire-and-forget)
521
-
522
- #### 6. Register Event Consumers
523
-
524
- Add consumers to your module providers - they're auto-discovered via `@EventHandler`:
536
+ **Non-critical consumers** — fire-and-forget. Failures are logged but don't affect the result.
525
537
 
526
538
  ```typescript
527
- @Module({
528
- imports: [NestMediatorModule.forRoot()],
529
- providers: [
530
- // Command handlers
531
- PlaceOrderHandler,
532
-
533
- // Event consumers - auto-discovered
534
- ValidateInventoryConsumer,
535
- ReserveInventoryConsumer,
536
- CreateOrderRecordConsumer,
537
- SendOrderConfirmationConsumer,
538
- NotifyWarehouseConsumer,
539
- TrackAnalyticsConsumer,
540
- ],
541
- })
542
- export class AppModule {}
539
+ @Injectable()
540
+ @EventHandler(OrderPlacedEvent)
541
+ @NonCritical()
542
+ export class SendConfirmationEmailHandler implements IEventConsumer<OrderPlacedEvent> {
543
+ async handle(event: OrderPlacedEvent): Promise<void> {
544
+ // Send email — failure doesn't block the order
545
+ }
546
+ }
543
547
  ```
544
548
 
545
- ## Complete Example
549
+ Consumers without `@Critical` or `@NonCritical` default to non-critical.
546
550
 
547
- Here's a complete example following Domain-Driven Design principles with proper separation of concerns:
548
-
549
- ### Project Structure
551
+ ### Event Execution Flow
550
552
 
551
553
  ```
552
- src/
553
- ├── domain/
554
- │ ├── entities/
555
- │ │ ├── user.ts
556
- │ │ └── index.ts
557
- └── exceptions/
558
- │ ├── domain.exception.ts
559
- │ ├── user-not-found.exception.ts
560
- │ └── index.ts
561
- ├── application/
562
- └── user/
563
- │ ├── create-user.command.ts
564
- │ ├── create-user.handler.ts
565
- │ ├── get-user.query.ts
566
- │ ├── get-user.handler.ts
567
- │ └── user-persistor.port.ts
568
- ├── infrastructure/
569
- │ └── persistence/
570
- │ └── user/
571
- │ └── user-persistence.adapter.ts
572
- ├── presentation/
573
- │ └── user/
574
- │ ├── create-user-api.request.ts
575
- │ ├── user-api.response.ts
576
- │ └── user.controller.ts
577
- └── app.module.ts
554
+ publish(OrderPlacedEvent)
555
+ |
556
+ |-> System Phase (event store persistence)
557
+ |
558
+ |-> Critical Consumers (sequential, awaited)
559
+ | |-> ReserveInventoryHandler (order: 1) ok [has compensation]
560
+ | |-> ProcessPaymentHandler (order: 2) FAILS
561
+ | |
562
+ | | On failure -> compensations in REVERSE order:
563
+ | | '-> ReserveInventoryHandler.applyCompensatingEvent()
564
+ | | -> publishes InventoryReleasedEvent
565
+ | | -> persisted, dispatched to its own consumers
566
+ | '-> Throw original error
567
+ |
568
+ '-> Non-Critical Consumers (parallel, fire-and-forget)
569
+ |-> SendConfirmationEmailHandler
570
+ '-> TrackAnalyticsHandler
571
+ Only runs if ALL critical consumers succeed
578
572
  ```
579
573
 
580
- ### Domain Layer
574
+ ### Saga-Style Compensation
581
575
 
582
- #### domain/entities/user.ts
576
+ Critical consumers can return compensating events when a later consumer in the chain fails. The library publishes these events through the full event flow — persisted, dispatched, traceable.
583
577
 
584
578
  ```typescript
585
- export class User {
586
- constructor(
587
- public readonly id: string,
588
- public readonly email: string,
589
- public readonly name: string,
590
- public readonly age: number,
591
- public readonly createdAt: Date
592
- ) {}
579
+ @Injectable()
580
+ @EventHandler(OrderPlacedEvent)
581
+ @Critical({ order: 1 })
582
+ export class ReserveInventoryHandler implements ICriticalEventConsumer<OrderPlacedEvent> {
583
+ constructor(private readonly mediatorBus: MediatorBus) {}
593
584
 
594
- static create(params: {
595
- id: string;
596
- email: string;
597
- name: string;
598
- age: number;
599
- }): User {
600
- const now = new Date();
601
- return new User(
602
- params.id,
603
- params.email,
604
- params.name,
605
- params.age,
606
- now
607
- );
585
+ async handle(event: OrderPlacedEvent): Promise<void> {
586
+ // Reserve inventory...
587
+ await this.mediatorBus.publish(new InventoryReservedEvent(event.orderId, event.items));
608
588
  }
609
- }
610
- ```
611
589
 
612
- #### domain/exceptions/domain.exception.ts
613
-
614
- ```typescript
615
- export class DomainException extends Error {
616
- constructor(message: string) {
617
- super(message);
618
- this.name = this.constructor.name;
590
+ // Return the compensating event — don't perform side effects here
591
+ async applyCompensatingEvent(event: OrderPlacedEvent): Promise<IEvent> {
592
+ return new InventoryReleasedEvent(event.orderId);
619
593
  }
620
594
  }
621
- ```
622
-
623
- #### domain/exceptions/user-not-found.exception.ts
624
595
 
625
- ```typescript
626
- import { DomainException } from './domain.exception';
627
-
628
- export class UserNotFoundException extends DomainException {
629
- constructor(userId: string) {
630
- super(`User with id ${userId} not found`);
596
+ // Dedicated consumer where the actual rollback logic lives
597
+ @Injectable()
598
+ @EventHandler(InventoryReleasedEvent)
599
+ export class HandleInventoryReleasedHandler implements IEventConsumer<InventoryReleasedEvent> {
600
+ async handle(event: InventoryReleasedEvent): Promise<void> {
601
+ // Release the reserved inventory
631
602
  }
632
603
  }
633
604
  ```
634
605
 
635
- ### Application Layer
636
-
637
- #### application/user/create-user.command.ts
638
-
639
- ```typescript
640
- import { ICommand } from '@rolandsall24/nest-mediator';
606
+ **Compensation rules:**
607
+ - Runs in **reverse order** (last succeeded -> first succeeded)
608
+ - The returned event is published through the full event flow (persisted + dispatched)
609
+ - Should be **idempotent**
610
+ - Errors in compensations are logged but don't stop other compensations
641
611
 
642
- export class CreateUserCommand implements ICommand {
643
- constructor(
644
- public readonly email: string,
645
- public readonly name: string,
646
- public readonly age: number
647
- ) {}
648
- }
649
- ```
612
+ ### Correlation & Causation IDs
650
613
 
651
- #### application/user/user-persistor.port.ts
614
+ The library automatically tracks two IDs through `AsyncLocalStorage` — no configuration needed.
652
615
 
653
- ```typescript
654
- import { User } from '../../domain/entities/user';
616
+ **Correlation ID** groups every event that stems from a single business transaction. When a command enters the system, a new `correlation_id` (UUID) is generated and propagated to every event published during that transaction, including events published by event handlers themselves.
655
617
 
656
- export interface UserPersistor {
657
- save(user: User): Promise<User>;
658
- findById(id: string): Promise<User | null>;
659
- }
618
+ **Causation ID** creates a parent-child link between events. When an event handler publishes a new event, the child's `causation_id` is set to the parent event's `event_id`. This lets you reconstruct the full causal chain.
660
619
 
661
- export const USER_PERSISTOR = Symbol('USER_PERSISTOR');
620
+ ```
621
+ PlaceOrderCommand correlation: abc
622
+ '-> OrderPlacedEvent correlation: abc, causation: null
623
+ |-> InventoryReservedEvent correlation: abc, causation: <OrderPlacedEvent.id>
624
+ '-> PaymentChargedEvent correlation: abc, causation: <OrderPlacedEvent.id>
625
+ '-> ReceiptGeneratedEvent correlation: abc, causation: <PaymentChargedEvent.id>
662
626
  ```
663
627
 
664
- #### application/user/create-user.handler.ts
628
+ Both IDs are stored in the event store automatically. This gives you:
629
+ - **Transaction tracing** — query all events with the same `correlation_id` to see everything that happened in one business operation
630
+ - **Causal ordering** — follow `causation_id` chains to understand what triggered what
631
+ - **Debugging** — when something fails, trace the full event chain that led to the failure
665
632
 
666
- ```typescript
667
- import { Injectable, Inject } from '@nestjs/common';
668
- import { CommandHandler, ICommandHandler } from '@rolandsall24/nest-mediator';
669
- import { randomUUID } from 'crypto';
670
- import { CreateUserCommand } from './create-user.command';
671
- import { User } from '../../domain/entities/user';
672
- import { UserPersistor, USER_PERSISTOR } from './user-persistor.port';
673
-
674
- @Injectable()
675
- @CommandHandler(CreateUserCommand)
676
- export class CreateUserCommandHandler implements ICommandHandler<CreateUserCommand> {
677
- constructor(
678
- @Inject(USER_PERSISTOR)
679
- private readonly userPersistor: UserPersistor
680
- ) {}
681
-
682
- async execute(command: CreateUserCommand): Promise<void> {
683
- const id = randomUUID();
684
-
685
- const user = User.create({
686
- id,
687
- email: command.email,
688
- name: command.name,
689
- age: command.age,
690
- });
691
-
692
- await this.userPersistor.save(user);
693
- }
694
- }
695
- ```
633
+ ---
696
634
 
697
- #### application/user/get-user.query.ts
635
+ ## Pipeline Behaviors
698
636
 
699
- ```typescript
700
- import { IQuery } from '@rolandsall24/nest-mediator';
637
+ Behaviors wrap around command and query handlers for cross-cutting concerns.
701
638
 
702
- export class GetUserQuery implements IQuery {
703
- constructor(public readonly id: string) {}
704
- }
705
- ```
706
-
707
- #### application/user/get-user.handler.ts
639
+ ### Built-in Behaviors
708
640
 
709
641
  ```typescript
710
- import { Injectable, Inject } from '@nestjs/common';
711
- import { QueryHandler, IQueryHandler } from '@rolandsall24/nest-mediator';
712
- import { GetUserQuery } from './get-user.query';
713
- import { User } from '../../domain/entities/user';
714
- import { UserNotFoundException } from '../../domain/exceptions/user-not-found.exception';
715
- import { UserPersistor, USER_PERSISTOR } from './user-persistor.port';
716
-
717
- @Injectable()
718
- @QueryHandler(GetUserQuery)
719
- export class GetUserQueryHandler implements IQueryHandler<GetUserQuery, User> {
720
- constructor(
721
- @Inject(USER_PERSISTOR)
722
- private readonly userPersistor: UserPersistor
723
- ) {}
724
-
725
- async execute(query: GetUserQuery): Promise<User> {
726
- const user = await this.userPersistor.findById(query.id);
727
-
728
- if (!user) {
729
- throw new UserNotFoundException(query.id);
730
- }
731
-
732
- return user;
733
- }
734
- }
642
+ NestMediatorModule.forRoot({
643
+ enableLogging: true, // Log requests with timing
644
+ enableValidation: true, // class-validator validation
645
+ enableExceptionHandling: true, // Centralized error logging
646
+ enablePerformanceTracking: true, // Slow request warnings
647
+ performanceThresholdMs: 500,
648
+ })
735
649
  ```
736
650
 
737
- ### Infrastructure Layer
651
+ | Behavior | Priority | Description |
652
+ |----------|----------|-------------|
653
+ | `ExceptionHandlingBehavior` | -100 | Catches and logs exceptions |
654
+ | `LoggingBehavior` | 0 | Logs request handling with timing |
655
+ | `PerformanceBehavior` | 10 | Warns on slow requests |
656
+ | `ValidationBehavior` | 100 | Validates using class-validator |
738
657
 
739
- #### infrastructure/persistence/user/user-persistence.adapter.ts
658
+ ### Custom Behaviors
740
659
 
741
660
  ```typescript
742
- import { Injectable } from '@nestjs/common';
743
- import { UserPersistor } from '../../../application/user/user-persistor.port';
744
- import { User } from '../../../domain/entities/user';
745
-
746
661
  @Injectable()
747
- export class UserPersistenceAdapter implements UserPersistor {
748
- // In-memory storage for demonstration
749
- private users: Map<string, User> = new Map();
750
-
751
- async save(user: User): Promise<User> {
752
- this.users.set(user.id, user);
753
- return user;
754
- }
755
-
756
- async findById(id: string): Promise<User | null> {
757
- return this.users.get(id) || null;
662
+ @PipelineBehavior({ priority: 50, scope: 'command' })
663
+ export class AuditLoggingBehavior<TRequest, TResponse>
664
+ implements IPipelineBehavior<TRequest, TResponse>
665
+ {
666
+ async handle(request: TRequest, next: () => Promise<TResponse>): Promise<TResponse> {
667
+ console.log(`Executing ${request.constructor.name}`);
668
+ const result = await next();
669
+ console.log(`Completed ${request.constructor.name}`);
670
+ return result;
758
671
  }
759
672
  }
760
673
  ```
761
674
 
762
- ### Presentation Layer
763
-
764
- #### presentation/user/create-user-api.request.ts
765
-
766
- ```typescript
767
- export class CreateUserApiRequest {
768
- email: string;
769
- name: string;
770
- age: number;
771
- }
772
- ```
773
-
774
- #### presentation/user/user-api.response.ts
775
-
776
- ```typescript
777
- export class UserApiResponse {
778
- id: string;
779
- email: string;
780
- name: string;
781
- age: number;
782
- createdAt: Date;
783
- }
784
- ```
675
+ ### Type-Specific Behaviors
785
676
 
786
- #### presentation/user/user.controller.ts
677
+ Add `@Handle()` to make a behavior only run for a specific request type:
787
678
 
788
679
  ```typescript
789
- import { Controller, Post, Body, Get, Param } from '@nestjs/common';
790
- import { MediatorBus } from '@rolandsall24/nest-mediator';
791
- import { CreateUserCommand } from '../../application/user/create-user.command';
792
- import { GetUserQuery } from '../../application/user/get-user.query';
793
- import { CreateUserApiRequest } from './create-user-api.request';
794
- import { UserApiResponse } from './user-api.response';
795
-
796
- @Controller('users')
797
- export class UserController {
798
- constructor(private readonly mediator: MediatorBus) {}
799
-
800
- @Post()
801
- async create(@Body() request: CreateUserApiRequest): Promise<void> {
802
- const command = new CreateUserCommand(
803
- request.email,
804
- request.name,
805
- request.age
806
- );
807
-
808
- await this.mediator.send(command);
809
- }
810
-
811
- @Get(':id')
812
- async getById(@Param('id') id: string): Promise<UserApiResponse> {
813
- const query = new GetUserQuery(id);
814
- const user = await this.mediator.query(query);
815
-
816
- return {
817
- id: user.id,
818
- email: user.email,
819
- name: user.name,
820
- age: user.age,
821
- createdAt: user.createdAt,
822
- };
680
+ @Injectable()
681
+ @PipelineBehavior({ priority: 95, scope: 'command' })
682
+ export class CreateUserValidation implements IPipelineBehavior<CreateUserCommand, void> {
683
+ @Handle() // Only runs for CreateUserCommand
684
+ async handle(request: CreateUserCommand, next: () => Promise<void>): Promise<void> {
685
+ if (!request.email.includes('@')) throw new Error('Invalid email');
686
+ return next();
823
687
  }
824
688
  }
825
689
  ```
826
690
 
827
- ### Module Configuration
828
-
829
- #### app.module.ts
691
+ ### Skip Behaviors
830
692
 
831
693
  ```typescript
832
- import { Module } from '@nestjs/common';
833
- import { NestMediatorModule } from '@rolandsall24/nest-mediator';
834
- import { UserController } from './presentation/user/user.controller';
835
- import { CreateUserCommandHandler } from './application/user/create-user.handler';
836
- import { GetUserQueryHandler } from './application/user/get-user.handler';
837
- import { USER_PERSISTOR } from './application/user/user-persistor.port';
838
- import { UserPersistenceAdapter } from './infrastructure/persistence/user/user-persistence.adapter';
839
-
840
- @Module({
841
- imports: [
842
- // Enable pipeline behaviors for logging, validation, and error handling
843
- NestMediatorModule.forRoot({
844
- enableLogging: true,
845
- enableValidation: true,
846
- enableExceptionHandling: true,
847
- }),
848
- ],
849
- controllers: [UserController],
850
- providers: [
851
- // Infrastructure
852
- {
853
- provide: USER_PERSISTOR,
854
- useClass: UserPersistenceAdapter,
855
- },
856
- // Handlers - automatically discovered and registered by the mediator
857
- CreateUserCommandHandler,
858
- GetUserQueryHandler,
859
- ],
860
- })
861
- export class AppModule {}
862
- ```
863
-
864
- ### Key Benefits
865
-
866
- 1. **Domain Layer**: Pure business logic, framework-agnostic
867
- - Entities contain business rules and invariants
868
- - Domain exceptions represent business errors
869
-
870
- 2. **Application Layer**: Use cases and business workflows
871
- - Commands/Queries define application operations
872
- - Handlers orchestrate domain objects and ports
873
- - Ports (interfaces) define contracts for infrastructure
874
-
875
- 3. **Infrastructure Layer**: Technical implementations
876
- - Adapters implement port interfaces
877
- - Database, external services, file systems, etc.
878
-
879
- 4. **Presentation Layer**: API interface
880
- - Controllers handle HTTP concerns
881
- - DTOs for API request/response
882
- - No business logic
883
-
884
- This separation enables:
885
- - Easy testing (mock ports/adapters)
886
- - Technology independence (swap databases/frameworks)
887
- - Clear boundaries and responsibilities
888
- - Scalable architecture for growing applications
889
-
890
- ## API Reference
891
-
892
- ### Interfaces
893
-
894
- #### `ICommand`
895
-
896
- Marker interface for commands.
897
-
898
- ```typescript
899
- export interface ICommand {}
900
- ```
901
-
902
- #### `ICommandHandler<TCommand>`
903
-
904
- Interface for command handlers.
905
-
906
- ```typescript
907
- export interface ICommandHandler<TCommand extends ICommand> {
908
- execute(command: TCommand): Promise<void>;
909
- }
910
- ```
911
-
912
- #### `IQuery`
913
-
914
- Marker interface for queries.
694
+ @SkipBehavior(PerformanceBehavior)
695
+ export class HighFrequencyCommand implements ICommand {}
915
696
 
916
- ```typescript
917
- export interface IQuery {}
697
+ @SkipBehavior([PerformanceBehavior, LoggingBehavior])
698
+ export class HealthCheckQuery implements IQuery {}
918
699
  ```
919
700
 
920
- #### `IQueryHandler<TQuery, TResult>`
921
-
922
- Interface for query handlers.
701
+ ### Priority Guidelines
923
702
 
924
- ```typescript
925
- export interface IQueryHandler<TQuery extends IQuery, TResult = any> {
926
- execute(query: TQuery): Promise<TResult>;
927
- }
928
703
  ```
929
-
930
- #### `IPipelineBehavior<TRequest, TResponse>`
931
-
932
- Interface for pipeline behaviors (cross-cutting concerns).
933
-
934
- ```typescript
935
- export interface IPipelineBehavior<TRequest = any, TResponse = any> {
936
- handle(request: TRequest, next: () => Promise<TResponse>): Promise<TResponse>;
937
- }
938
- ```
939
-
940
- #### `IEvent`
941
-
942
- Marker interface for domain events.
943
-
944
- ```typescript
945
- export interface IEvent {}
704
+ -100 to -1: Exception handling (outermost)
705
+ 0 to 99: Logging, performance tracking
706
+ 100 to 199: Validation
707
+ 200+: Transaction / unit of work (innermost)
946
708
  ```
947
709
 
948
- #### `IEventConsumer<TEvent>`
949
-
950
- Interface for event consumers (non-critical or critical without compensation).
710
+ ---
951
711
 
952
- ```typescript
953
- export interface IEventConsumer<TEvent extends IEvent> {
954
- handle(event: TEvent): Promise<void>;
955
- }
956
- ```
712
+ ## Event Store Configuration
957
713
 
958
- #### `ICriticalEventConsumer<TEvent>`
714
+ The event store is flexible — you control how it connects, what repository it uses, and what table it writes to.
959
715
 
960
- Interface for critical event consumers with optional compensation support (saga pattern).
716
+ ### Connection Options
961
717
 
962
718
  ```typescript
963
- export interface ICriticalEventConsumer<TEvent extends IEvent> extends IEventConsumer<TEvent> {
964
- /**
965
- * Compensate/rollback the work done by handle().
966
- * Called when a subsequent critical consumer fails.
967
- * Should be idempotent and derive state from the event.
968
- */
969
- compensate?(event: TEvent): Promise<void>;
719
+ // Option 1: Library manages the connection pool
720
+ eventStore: {
721
+ type: 'postgres',
722
+ url: 'postgres://user:pass@localhost:5432/mydb',
723
+ mode: 'source',
970
724
  }
971
- ```
972
725
 
973
- #### `EventPublishResult`
974
-
975
- Result returned by `publish()`.
976
-
977
- ```typescript
978
- export interface EventPublishResult {
979
- totalHandlers: number;
980
- criticalSucceeded: number;
981
- nonCriticalDispatched: number;
982
- compensationsRun: number; // Number of compensations executed on failure
726
+ // Option 2: Reuse an existing connection pool from your app
727
+ eventStore: {
728
+ type: 'postgres',
729
+ useExistingPool: 'DATABASE_POOL', // your DI token
730
+ mode: 'audit',
983
731
  }
984
- ```
985
-
986
- #### `EventCriticality`
987
-
988
- Enum for event consumer criticality.
989
732
 
990
- ```typescript
991
- export enum EventCriticality {
992
- CRITICAL = 'critical',
993
- NON_CRITICAL = 'non-critical',
733
+ // Option 3: Bring your own IEventStoreRepository implementation
734
+ eventStore: {
735
+ type: 'postgres',
736
+ url: 'postgres://...', // used only for schema creation
737
+ useExistingRepository: 'MY_EVENT_STORE', // your DI token
738
+ mode: 'source',
994
739
  }
995
740
  ```
996
741
 
997
- ### Decorators
998
-
999
- #### `@CommandHandler(command)`
1000
-
1001
- Marks a class as a command handler.
1002
-
1003
- - **Parameters**: `command` - The command class this handler handles
1004
- - **Usage**: Apply to handler classes that implement `ICommandHandler`
1005
-
1006
- #### `@QueryHandler(query)`
1007
-
1008
- Marks a class as a query handler.
1009
-
1010
- - **Parameters**: `query` - The query class this handler handles
1011
- - **Usage**: Apply to handler classes that implement `IQueryHandler`
1012
-
1013
- #### `@PipelineBehavior(options?)`
1014
-
1015
- Marks a class as a pipeline behavior.
1016
-
1017
- - **Parameters**:
1018
- - `options.priority` - Execution order (lower numbers execute first, default: 0)
1019
- - `options.scope` - `'command'`, `'query'`, or `'all'` (default: `'all'`)
1020
- - **Usage**: Apply to behavior classes that implement `IPipelineBehavior`
1021
-
1022
- #### `@Handle()`
1023
-
1024
- Method decorator that enables automatic request type inference for pipeline behaviors.
742
+ ### Custom Event Store Repository
1025
743
 
1026
- - **Parameters**: None
1027
- - **Usage**: Apply to the `handle` method to make the behavior type-specific
744
+ If the built-in PostgreSQL repository doesn't fit your needs, implement `IEventStoreRepository` and register it yourself:
1028
745
 
1029
746
  ```typescript
1030
- // Generic behavior - applies to ALL requests in scope
1031
747
  @Injectable()
1032
- @PipelineBehavior({ priority: 0 })
1033
- export class LoggingBehavior<TRequest, TResponse>
1034
- implements IPipelineBehavior<TRequest, TResponse> {
1035
- async handle(request: TRequest, next: () => Promise<TResponse>) {
1036
- // Runs for all requests
1037
- return next();
1038
- }
748
+ export class MyEventStoreRepository implements IEventStoreRepository {
749
+ async saveEvent(event: StoredEvent): Promise<void> { /* ... */ }
750
+ async appendEvents(aggregateType: string, aggregateId: string,
751
+ events: StoredEvent[], expectedVersion: number): Promise<void> { /* ... */ }
752
+ async getEventsForAggregate(aggregateType: string,
753
+ aggregateId: string): Promise<StoredEvent[]> { /* ... */ }
754
+ async getNextSequence(aggregateType: string,
755
+ aggregateId: string): Promise<number> { /* ... */ }
1039
756
  }
1040
757
 
1041
- // Type-specific behavior - applies ONLY to CreateUserCommand
1042
- @Injectable()
1043
- @PipelineBehavior({ priority: 100, scope: 'command' })
1044
- export class CreateUserValidationBehavior
1045
- implements IPipelineBehavior<CreateUserCommand, void> {
1046
- @Handle() // <-- Enables type inference
1047
- async handle(request: CreateUserCommand, next: () => Promise<void>) {
1048
- // Only runs for CreateUserCommand
1049
- return next();
1050
- }
1051
- }
1052
- ```
1053
-
1054
- #### `@SkipBehavior(behavior | behaviors[])`
1055
-
1056
- Excludes specific pipeline behaviors from a command or query.
1057
-
1058
- - **Parameters**:
1059
- - Single behavior class: `@SkipBehavior(PerformanceBehavior)`
1060
- - Array of behavior classes: `@SkipBehavior([PerformanceBehavior, LoggingBehavior])`
1061
- - **Usage**: Apply to command or query classes
1062
- - **Works with**: Both built-in behaviors and custom behaviors
1063
-
1064
- #### `@EventHandler(event)`
1065
-
1066
- Marks a class as an event consumer.
1067
-
1068
- - **Parameters**: `event` - The event class this consumer handles
1069
- - **Usage**: Apply to consumer classes that implement `IEventConsumer`
1070
-
1071
- #### `@Critical(options?)`
1072
-
1073
- Marks an event consumer as critical. Critical consumers run sequentially in order.
1074
-
1075
- - **Parameters**:
1076
- - `options.order` - Execution order among critical consumers (lower numbers first, default: 0)
1077
- - **Usage**: Apply to consumer classes alongside `@EventHandler`
1078
- - **Behavior**: If a critical consumer fails, remaining critical consumers are skipped and non-critical consumers don't run
1079
-
1080
- #### `@NonCritical()`
1081
-
1082
- Marks an event consumer as non-critical. Non-critical consumers run in parallel after critical consumers complete.
1083
-
1084
- - **Parameters**: None
1085
- - **Usage**: Apply to consumer classes alongside `@EventHandler`
1086
- - **Behavior**: Fire-and-forget - failures are logged but don't affect the publish result
1087
- - **Note**: Consumers without `@Critical` or `@NonCritical` default to non-critical
1088
-
1089
- ### Services
1090
-
1091
- #### `MediatorBus`
1092
-
1093
- The main service for sending commands, queries, and events.
1094
-
1095
- ##### Methods
1096
-
1097
- **`send<TCommand>(command: TCommand): Promise<void>`**
1098
-
1099
- Sends a command to its registered handler.
1100
-
1101
- - **Parameters**: `command` - The command instance to execute
1102
- - **Returns**: Promise that resolves when the command is executed
1103
- - **Throws**: `HandlerNotFoundException` if no handler is registered for the command
1104
-
1105
- **`query<TQuery, TResult>(query: TQuery): Promise<TResult>`**
1106
-
1107
- Executes a query through its registered handler.
1108
-
1109
- - **Parameters**: `query` - The query instance to execute
1110
- - **Returns**: Promise that resolves with the query result
1111
- - **Throws**: `HandlerNotFoundException` if no handler is registered for the query
1112
-
1113
- **`publish<TEvent>(event: TEvent): Promise<EventPublishResult>`**
1114
-
1115
- Publishes an event to all registered consumers.
1116
-
1117
- - **Parameters**: `event` - The event instance to publish
1118
- - **Returns**: Promise with `EventPublishResult` containing:
1119
- - `totalHandlers` - Total number of consumers
1120
- - `criticalSucceeded` - Number of critical consumers that completed
1121
- - `nonCriticalDispatched` - Number of non-critical consumers dispatched
1122
- - **Throws**: Error if any critical consumer fails
1123
-
1124
- ### Module Configuration
1125
-
1126
- #### `NestMediatorModule.forRoot()`
1127
-
1128
- Basic module registration with no built-in behaviors.
1129
-
1130
- #### `NestMediatorModule.forRoot(options)`
1131
-
1132
- Module registration with configuration options.
1133
-
1134
- | Option | Type | Default | Description |
1135
- |--------|------|---------|-------------|
1136
- | `enableLogging` | boolean | false | Enable request/response logging |
1137
- | `enableValidation` | boolean | false | Enable class-validator validation |
1138
- | `enableExceptionHandling` | boolean | false | Enable centralized exception logging |
1139
- | `enablePerformanceTracking` | boolean | false | Enable slow request warnings |
1140
- | `performanceThresholdMs` | number | 500 | Threshold for slow request warnings |
1141
-
1142
- ## Pipeline Behaviors
1143
-
1144
- Pipeline behaviors allow you to add cross-cutting concerns (like logging, validation, caching) that execute around every command and query handler.
1145
-
1146
- ### Enabling Built-in Behaviors
1147
-
1148
- ```typescript
1149
- import { Module } from '@nestjs/common';
1150
- import { NestMediatorModule } from '@rolandsall24/nest-mediator';
1151
-
758
+ // Register in your module
1152
759
  @Module({
1153
- imports: [
1154
- NestMediatorModule.forRoot({
1155
- enableLogging: true, // Logs request handling with timing
1156
- enableValidation: true, // Validates requests using class-validator
1157
- enableExceptionHandling: true, // Centralized exception logging
1158
- enablePerformanceTracking: true, // Warns on slow requests
1159
- performanceThresholdMs: 500, // Threshold for slow request warnings
1160
- }),
760
+ providers: [
761
+ { provide: 'MY_EVENT_STORE', useClass: MyEventStoreRepository },
1161
762
  ],
1162
763
  })
1163
- export class AppModule {}
1164
- ```
1165
-
1166
- ### Built-in Behaviors
1167
-
1168
- | Behavior | Priority | Description |
1169
- |----------|----------|-------------|
1170
- | `ExceptionHandlingBehavior` | -100 | Catches and logs all exceptions |
1171
- | `LoggingBehavior` | 0 | Logs request handling with timing |
1172
- | `PerformanceBehavior` | 10 | Warns when requests exceed threshold |
1173
- | `ValidationBehavior` | 100 | Validates requests using class-validator |
1174
-
1175
- ### Creating Custom Behaviors
1176
-
1177
- ```typescript
1178
- import { Injectable } from '@nestjs/common';
1179
- import { IPipelineBehavior, PipelineBehavior } from '@rolandsall24/nest-mediator';
1180
-
1181
- @Injectable()
1182
- @PipelineBehavior({ priority: 50, scope: 'all' })
1183
- export class MyCustomBehavior<TRequest, TResponse>
1184
- implements IPipelineBehavior<TRequest, TResponse> {
1185
-
1186
- async handle(
1187
- request: TRequest,
1188
- next: () => Promise<TResponse>
1189
- ): Promise<TResponse> {
1190
- // Pre-processing logic
1191
- console.log('Before handler:', request);
1192
-
1193
- // Call the next behavior or handler
1194
- const response = await next();
1195
-
1196
- // Post-processing logic
1197
- console.log('After handler:', response);
1198
-
1199
- return response;
1200
- }
1201
- }
1202
- ```
1203
-
1204
- ### Behavior Options
1205
-
1206
- ```typescript
1207
- @PipelineBehavior({
1208
- priority: 50, // Lower numbers execute first (outermost)
1209
- scope: 'command', // 'command', 'query', or 'all'
764
+ export class PersistenceModule {}
765
+
766
+ // Reference in config
767
+ NestMediatorModule.forRoot({
768
+ eventStore: {
769
+ type: 'postgres',
770
+ url: process.env.DATABASE_URL, // schema creation only
771
+ useExistingRepository: 'MY_EVENT_STORE',
772
+ mode: 'source',
773
+ },
1210
774
  })
1211
775
  ```
1212
776
 
1213
- **Priority Guidelines:**
1214
- - `-100 to -1`: Exception handling (outermost)
1215
- - `0 to 99`: Logging, performance tracking
1216
- - `100 to 199`: Validation
1217
- - `200+`: Transaction/Unit of Work (innermost)
1218
-
1219
- ### Registering Custom Behaviors
777
+ The library creates the schema using a temporary connection, then delegates all operations to your repository. The `AggregateRepository` base class injects the `EVENT_STORE_REPOSITORY` token, which resolves to whatever you provide.
1220
778
 
1221
- Custom behaviors with the `@PipelineBehavior` decorator are auto-discovered. Just add them to your module's providers:
779
+ ### Custom Repository Class
1222
780
 
1223
- ```typescript
1224
- @Module({
1225
- imports: [NestMediatorModule.forRoot()],
1226
- providers: [MyCustomBehavior], // Auto-discovered via @PipelineBehavior decorator
1227
- })
1228
- export class AppModule {}
1229
- ```
1230
-
1231
- ### Skipping Behaviors for Specific Commands/Queries
1232
-
1233
- Use `@SkipBehavior` to exclude specific behaviors from a command or query:
781
+ Alternatively, pass a class and let the library instantiate it with the pool:
1234
782
 
1235
783
  ```typescript
1236
- import {
1237
- ICommand,
1238
- SkipBehavior,
1239
- PerformanceBehavior,
1240
- LoggingBehavior,
1241
- } from '@rolandsall24/nest-mediator';
1242
-
1243
- // Skip a single behavior
1244
- @SkipBehavior(PerformanceBehavior)
1245
- export class HighFrequencyCommand implements ICommand {
1246
- // This command will not trigger performance tracking
1247
- }
1248
-
1249
- // Skip multiple behaviors
1250
- @SkipBehavior([PerformanceBehavior, LoggingBehavior])
1251
- export class HealthCheckQuery implements IQuery {
1252
- // This query skips both performance and logging behaviors
784
+ eventStore: {
785
+ type: 'postgres',
786
+ url: process.env.DATABASE_URL,
787
+ repository: MyCustomPostgresEventStore, // must implement IEventStoreRepository
788
+ mode: 'source',
1253
789
  }
1254
790
  ```
1255
791
 
1256
- This works with both built-in behaviors and custom behaviors:
792
+ ### Full Configuration Reference
1257
793
 
1258
794
  ```typescript
1259
- import { SkipBehavior } from '@rolandsall24/nest-mediator';
1260
- import { MyAuditBehavior } from './behaviors/audit.behavior';
1261
-
1262
- @SkipBehavior(MyAuditBehavior)
1263
- export class InternalCommand implements ICommand {
1264
- // Skips your custom audit behavior
1265
- }
795
+ NestMediatorModule.forRoot({
796
+ enableLogging?: boolean, // default: false
797
+ enableValidation?: boolean, // default: false
798
+ enableExceptionHandling?: boolean, // default: false
799
+ enablePerformanceTracking?: boolean, // default: false
800
+ performanceThresholdMs?: number, // default: 500
801
+ eventStore?: {
802
+ type: 'postgres',
803
+ url?: string, // library-managed pool
804
+ useExistingPool?: string, // reuse your pool (DI token)
805
+ useExistingRepository?: string, // bring your own repo (DI token)
806
+ repository?: Type<IEventStoreRepository>, // custom repo class
807
+ mode?: 'audit' | 'source', // default: 'audit'
808
+ tableName?: string, // default: 'domain_events'
809
+ },
810
+ })
1266
811
  ```
1267
812
 
1268
- ### Custom Behavior with Service Injection
1269
-
1270
- Behaviors support full NestJS dependency injection:
813
+ ---
1271
814
 
1272
- ```typescript
1273
- import { Injectable, Logger } from '@nestjs/common';
1274
- import { IPipelineBehavior, PipelineBehavior } from '@rolandsall24/nest-mediator';
815
+ ## API Reference
1275
816
 
1276
- // Service for audit logging
1277
- @Injectable()
1278
- export class AuditService {
1279
- private readonly logger = new Logger(AuditService.name);
817
+ ### Interfaces
1280
818
 
1281
- async logAction(action: string, userId: string): Promise<void> {
1282
- this.logger.log(`[AUDIT] ${action} by ${userId}`);
1283
- // Save to database, send to external service, etc.
1284
- }
1285
- }
819
+ | Interface | Description |
820
+ |-----------|-------------|
821
+ | `ICommand` | Marker interface for commands |
822
+ | `ICommandHandler<T>` | `execute(command: T): Promise<void>` |
823
+ | `IQuery` | Marker interface for queries |
824
+ | `IQueryHandler<T, R>` | `execute(query: T): Promise<R>` |
825
+ | `IEvent` | Marker interface for events |
826
+ | `IEventConsumer<T>` | `handle(event: T): Promise<void>` |
827
+ | `ICriticalEventConsumer<T>` | Extends `IEventConsumer` with `applyCompensatingEvent()` |
828
+ | `IPipelineBehavior<T, R>` | `handle(request: T, next: () => Promise<R>): Promise<R>` |
829
+ | `IEventStoreRepository` | `saveEvent()`, `appendEvents()`, `getEventsForAggregate()`, `getNextSequence()` |
830
+ | `AggregateRoot<TId>` | Base class for event-sourced aggregates |
831
+ | `AggregateRepository<T, TId>` | Base class for aggregate repositories |
1286
832
 
1287
- // Behavior that uses the service
1288
- @Injectable()
1289
- @PipelineBehavior({ priority: 50, scope: 'command' })
1290
- export class AuditLoggingBehavior<TRequest, TResponse>
1291
- implements IPipelineBehavior<TRequest, TResponse> {
833
+ ### Decorators
1292
834
 
1293
- constructor(private readonly auditService: AuditService) {}
835
+ | Decorator | Target | Description |
836
+ |-----------|--------|-------------|
837
+ | `@CommandHandler(CommandClass)` | Class | Registers a command handler |
838
+ | `@QueryHandler(QueryClass)` | Class | Registers a query handler |
839
+ | `@EventHandler(EventClass)` | Class | Registers an event consumer |
840
+ | `@Critical({ order: n })` | Class | Marks consumer as critical (sequential) |
841
+ | `@NonCritical()` | Class | Marks consumer as non-critical (fire-and-forget) |
842
+ | `@DomainEvent(aggregate, idProp)` | Class | Associates event with aggregate + registers in event registry. Required for source mode; optional in audit mode (adds `aggregate_type`/`aggregate_id` to logs) |
843
+ | `@ForAggregate(AggregateClass)` | Class | Wires an `AggregateRepository` to its aggregate (zero-boilerplate) |
844
+ | `@PipelineBehavior(options)` | Class | Registers a pipeline behavior |
845
+ | `@Handle()` | Method | Enables type-specific behavior filtering |
846
+ | `@SkipBehavior(behavior)` | Class | Excludes behaviors from a command/query |
847
+
848
+ ### MediatorBus
849
+
850
+ | Method | Description |
851
+ |--------|-------------|
852
+ | `send<T>(command: T)` | Execute a command |
853
+ | `query<T, R>(query: T)` | Execute a query |
854
+ | `publish<T>(event: T)` | Publish an event to all consumers |
1294
855
 
1295
- async handle(
1296
- request: TRequest,
1297
- next: () => Promise<TResponse>,
1298
- ): Promise<TResponse> {
1299
- const requestName = request.constructor.name;
856
+ ---
1300
857
 
1301
- await this.auditService.logAction(`Executing ${requestName}`, 'user-123');
858
+ ## Example Projects
1302
859
 
1303
- const response = await next();
860
+ The repository includes two complete example projects demonstrating clean architecture (`domain/`, `application/`, `infrastructure/`, `presentation/`):
1304
861
 
1305
- await this.auditService.logAction(`Completed ${requestName}`, 'user-123');
862
+ ### `example-audit/`
1306
863
 
1307
- return response;
1308
- }
1309
- }
864
+ **Audit mode** — Orders stored in a PostgreSQL `orders` table. Events logged to `audit_events` for traceability.
1310
865
 
1311
- // Register both in your module
1312
- @Module({
1313
- imports: [NestMediatorModule.forRoot()],
1314
- providers: [
1315
- AuditService, // The service
1316
- AuditLoggingBehavior, // The behavior (auto-discovered)
1317
- ],
1318
- })
1319
- export class AppModule {}
866
+ ```bash
867
+ cd example-audit
868
+ docker compose up -d # PostgreSQL on port 5433
869
+ DATABASE_URL=postgres://postgres:postgres@localhost:5433/audit_example npm run start:dev
1320
870
  ```
1321
871
 
1322
- ### Advanced Behavior Examples
872
+ | Endpoint | Description |
873
+ |----------|-------------|
874
+ | `POST /orders` | Create an order (saved to `orders` table + event logged) |
875
+ | `POST /orders/:id/cancel` | Cancel an order |
876
+ | `GET /orders/:id` | Get order from `orders` table |
877
+ | `GET /orders` | List all orders |
878
+ | `GET /internals/events` | View audit event log |
1323
879
 
1324
- #### Retry Behavior (for transient failures)
880
+ ### `example-source/`
1325
881
 
1326
- ```typescript
1327
- @Injectable()
1328
- @PipelineBehavior({ priority: -50, scope: 'command' })
1329
- export class RetryBehavior<TRequest, TResponse>
1330
- implements IPipelineBehavior<TRequest, TResponse> {
1331
-
1332
- private readonly maxRetries = 3;
1333
-
1334
- async handle(
1335
- request: TRequest,
1336
- next: () => Promise<TResponse>,
1337
- ): Promise<TResponse> {
1338
- let lastError: Error;
1339
-
1340
- for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
1341
- try {
1342
- return await next();
1343
- } catch (error) {
1344
- lastError = error as Error;
1345
-
1346
- // Don't retry validation errors
1347
- if (error.name?.includes('Validation')) throw error;
1348
-
1349
- if (attempt < this.maxRetries) {
1350
- const delay = 100 * Math.pow(2, attempt - 1); // Exponential backoff
1351
- await new Promise(resolve => setTimeout(resolve, delay));
1352
- }
1353
- }
1354
- }
882
+ **Source mode** — No `orders` table. State rebuilt entirely from events in `domain_events`.
1355
883
 
1356
- throw lastError!;
1357
- }
1358
- }
884
+ ```bash
885
+ cd example-source
886
+ docker compose up -d # PostgreSQL on port 5434
887
+ DATABASE_URL=postgres://postgres:postgres@localhost:5434/source_example npm run start:dev
1359
888
  ```
1360
889
 
1361
- #### Caching Behavior (for queries)
1362
-
1363
- ```typescript
1364
- @Injectable()
1365
- @PipelineBehavior({ priority: 5, scope: 'query' })
1366
- export class CachingBehavior<TRequest, TResponse>
1367
- implements IPipelineBehavior<TRequest, TResponse> {
1368
-
1369
- private cache = new Map<string, { data: any; expiry: number }>();
890
+ | Endpoint | Description |
891
+ |----------|-------------|
892
+ | `POST /orders` | Place an order (event persisted, state rebuilt from events) |
893
+ | `POST /orders/:id/cancel` | Cancel via aggregate pattern (load -> validate -> save) |
894
+ | `GET /orders/:id` | Get order state (rebuilt by replaying events) |
895
+ | `GET /orders/:id/events` | View raw event stream for an order |
896
+ | `POST /orders/test-concurrency` | Demo: fires two cancels simultaneously, one gets `ConcurrencyError` |
1370
897
 
1371
- async handle(
1372
- request: TRequest,
1373
- next: () => Promise<TResponse>,
1374
- ): Promise<TResponse> {
1375
- const key = JSON.stringify(request);
1376
- const cached = this.cache.get(key);
898
+ ---
1377
899
 
1378
- if (cached && cached.expiry > Date.now()) {
1379
- return cached.data; // Cache hit
1380
- }
900
+ ## Migrating to v1.0.0
1381
901
 
1382
- const result = await next();
902
+ ### `AggregateRepository` zero-boilerplate with `@ForAggregate`
1383
903
 
1384
- this.cache.set(key, {
1385
- data: result,
1386
- expiry: Date.now() + 30000, // 30 second TTL
1387
- });
1388
-
1389
- return result;
1390
- }
1391
- }
1392
- ```
904
+ The `AggregateRepository` no longer requires a constructor, `aggregateType`, `createEmptyAggregate()`, or `deserializeEvent()` overrides. Use the `@ForAggregate` decorator instead.
1393
905
 
1394
- #### Authorization Behavior
906
+ **Before:**
1395
907
 
1396
908
  ```typescript
1397
909
  @Injectable()
1398
- @PipelineBehavior({ priority: 25, scope: 'all' })
1399
- export class AuthorizationBehavior<TRequest, TResponse>
1400
- implements IPipelineBehavior<TRequest, TResponse> {
1401
-
1402
- constructor(private readonly authService: AuthService) {}
1403
-
1404
- private readonly adminOnlyCommands = ['DeleteUserCommand'];
1405
-
1406
- async handle(
1407
- request: TRequest,
1408
- next: () => Promise<TResponse>,
1409
- ): Promise<TResponse> {
1410
- const requestName = request.constructor.name;
910
+ export class OrderAggregateRepository
911
+ extends AggregateRepository<OrderAggregate, string>
912
+ {
913
+ protected readonly aggregateType = 'Order';
1411
914
 
1412
- if (!this.authService.isAuthenticated()) {
1413
- throw new UnauthorizedException('Authentication required');
1414
- }
915
+ constructor(
916
+ @Inject(EVENT_STORE_REPOSITORY) eventStore: IEventStoreRepository,
917
+ mediatorBus: MediatorBus,
918
+ ) {
919
+ super(eventStore, mediatorBus);
920
+ }
1415
921
 
1416
- if (this.adminOnlyCommands.includes(requestName)) {
1417
- if (!this.authService.hasRole('admin')) {
1418
- throw new ForbiddenException('Admin role required');
1419
- }
1420
- }
922
+ protected createEmptyAggregate(): OrderAggregate {
923
+ return new OrderAggregate();
924
+ }
1421
925
 
1422
- return next();
926
+ protected deserializeEvent(eventType: string, payload: Record<string, unknown>): IEvent {
927
+ const types = { OrderPlacedEvent, OrderCancelledEvent };
928
+ const EventClass = types[eventType];
929
+ if (!EventClass) throw new Error(`Unknown event type: ${eventType}`);
930
+ return Object.assign(Object.create(EventClass.prototype), payload);
1423
931
  }
1424
932
  }
1425
933
  ```
1426
934
 
1427
- ### Type-Specific Behaviors
1428
-
1429
- By default, behaviors apply to all requests matching their scope. To create a behavior that only applies to specific request types, add `@Handle()` to the `handle` method:
935
+ **After:**
1430
936
 
1431
937
  ```typescript
1432
- import { Injectable } from '@nestjs/common';
1433
- import { IPipelineBehavior, PipelineBehavior, Handle } from '@rolandsall24/nest-mediator';
1434
- import { CreateUserCommand } from './create-user.command';
1435
-
1436
938
  @Injectable()
1437
- @PipelineBehavior({ priority: 95, scope: 'command' })
1438
- export class CreateUserValidationBehavior
1439
- implements IPipelineBehavior<CreateUserCommand, void>
1440
- {
1441
- @Handle() // Enables type inference from method signature
1442
- async handle(
1443
- request: CreateUserCommand,
1444
- next: () => Promise<void>,
1445
- ): Promise<void> {
1446
- // This behavior ONLY runs for CreateUserCommand instances
1447
- // No manual instanceof check needed!
1448
-
1449
- const errors: string[] = [];
1450
-
1451
- if (!request.name || request.name.length < 2) {
1452
- errors.push('Name must be at least 2 characters');
1453
- }
1454
-
1455
- if (!request.email || !request.email.includes('@')) {
1456
- errors.push('Valid email is required');
1457
- }
1458
-
1459
- if (errors.length > 0) {
1460
- throw new Error(`Validation failed: ${errors.join(', ')}`);
1461
- }
1462
-
1463
- return next();
1464
- }
1465
- }
939
+ @ForAggregate(OrderAggregate)
940
+ export class OrderAggregateRepository
941
+ extends AggregateRepository<OrderAggregate, string> {}
1466
942
  ```
1467
943
 
1468
- **How it works:**
1469
-
1470
- 1. The `@Handle()` decorator on the `handle` method triggers TypeScript to emit `design:paramtypes` metadata
1471
- 2. At module initialization, the library reads this metadata to determine the request type (`CreateUserCommand`)
1472
- 3. During pipeline execution, the behavior is only included when `request instanceof CreateUserCommand` is true
1473
-
1474
- **Requirements:**
1475
- - TypeScript `emitDecoratorMetadata: true` must be enabled in tsconfig.json
1476
- - The request parameter must be a concrete class (not an interface or `any`)
944
+ The base class now uses property injection (`@Inject`) for `eventStore` and `eventBus`, derives `aggregateType` from the aggregate class, and uses the `@DomainEvent` registry for automatic event deserialization. All methods remain overridable for custom logic.
1477
945
 
1478
- **Comparison:**
946
+ ### `compensate()` -> `applyCompensatingEvent()`
1479
947
 
1480
- | Without `@Handle()` | With `@Handle()` |
1481
- |---------------------|------------------|
1482
- | Behavior runs for ALL commands | Behavior runs ONLY for specified type |
1483
- | Must use `instanceof` check inside handler | No `instanceof` check needed |
1484
- | Generic `<TRequest, TResponse>` | Specific type like `<CreateUserCommand, void>` |
948
+ The `compensate()` method on `ICriticalEventConsumer` is **deprecated**. It performed side effects directly, which were not captured in the event store and not traceable.
1485
949
 
1486
- ### Complete Behavior Execution Order Example
950
+ Use `applyCompensatingEvent()` instead it returns a compensating event that the library publishes through the full event flow (persisted, dispatched, traceable).
1487
951
 
1488
- With the following behaviors configured:
952
+ **Before (deprecated):**
1489
953
 
1490
954
  ```typescript
1491
- // Built-in behaviors (when enabled):
1492
- // ExceptionHandlingBehavior: priority -100, scope 'all'
1493
- // LoggingBehavior: priority 0, scope 'all'
1494
- // PerformanceBehavior: priority 10, scope 'all'
1495
- // ValidationBehavior: priority 100, scope 'all'
1496
-
1497
- // Custom behaviors:
1498
- // RetryBehavior: priority -50, scope 'command'
1499
- // CachingBehavior: priority 5, scope 'query'
1500
- // AuthorizationBehavior: priority 25, scope 'all'
1501
- // AuditLoggingBehavior: priority 50, scope 'command'
1502
- ```
1503
-
1504
- **For a Command:**
1505
- ```
1506
- Request → Exception(-100) → Retry(-50) → Logging(0) → Performance(10) → Authorization(25) → Audit(50) → Validation(100) → Handler
1507
- ```
1508
-
1509
- **For a Query:**
1510
- ```
1511
- Request → Exception(-100) → Logging(0) → Caching(5) → Performance(10) → Authorization(25) → Validation(100) → Handler
955
+ async compensate(event: OrderPlacedEvent): Promise<void> {
956
+ await this.inventoryService.release(event.orderId); // Side effect, not persisted
957
+ }
1512
958
  ```
1513
959
 
1514
- ### Validation with class-validator
1515
-
1516
- When `enableValidation` is true, requests are validated using class-validator (if installed):
960
+ **After:**
1517
961
 
1518
962
  ```typescript
1519
- import { IsEmail, IsString, MinLength } from 'class-validator';
1520
- import { ICommand } from '@rolandsall24/nest-mediator';
1521
-
1522
- export class CreateUserCommand implements ICommand {
1523
- @IsEmail()
1524
- email: string;
1525
-
1526
- @IsString()
1527
- @MinLength(2)
1528
- name: string;
1529
-
1530
- constructor(email: string, name: string) {
1531
- this.email = email;
1532
- this.name = name;
1533
- }
963
+ async applyCompensatingEvent(event: OrderPlacedEvent): Promise<IEvent> {
964
+ return new InventoryReleasedEvent(event.orderId); // Event published through full flow
1534
965
  }
1535
966
 
1536
- // If validation fails, ValidationException is thrown with details
1537
- ```
1538
-
1539
- ### How `next()` Works in Pipeline Behaviors
1540
-
1541
- The `next()` function is a delegate that invokes the next behavior in the pipeline (or the final handler). Here's how it works:
1542
-
1543
- ```
1544
- ┌─────────────────────────────────────────────────────────────────────────┐
1545
- │ REQUEST FLOW (→) │
1546
- │ │
1547
- │ Request │
1548
- │ │ │
1549
- │ ▼ │
1550
- │ ┌──────────────────────┐ │
1551
- │ │ Behavior A │ 1. Pre-processing (before next()) │
1552
- │ │ (priority: -100) │ - Wrap in try/catch │
1553
- │ │ │ - Start timer │
1554
- │ │ return next() ──────┼──► │
1555
- │ └──────────────────────┘ │
1556
- │ │ │
1557
- │ ▼ │
1558
- │ ┌──────────────────────┐ │
1559
- │ │ Behavior B │ 2. Pre-processing │
1560
- │ │ (priority: 0) │ - Log request │
1561
- │ │ │ │
1562
- │ │ return next() ──────┼──► │
1563
- │ └──────────────────────┘ │
1564
- │ │ │
1565
- │ ▼ │
1566
- │ ┌──────────────────────┐ │
1567
- │ │ Behavior C │ │
1568
- │ │ (priority: 100) │ │
1569
- │ │ │ │
1570
- │ │ return next() ──────┼──►
1571
- │ └──────────────────────┘ │
1572
- │ │
1573
- │ │ │
1574
- │ ▼ │
1575
- │ ┌──────────────────────┐ │
1576
- │ │ HANDLER │ │
1577
- │ │ execute(request) │ │
1578
- │ │ │ │
1579
- │ │ return result ◄─────┼──┤
1580
- │ └──────────────────────┘ │
1581
- │ │
1582
- └─────────────────────────────────────────────────────────────────────────┘
1583
-
1584
- ┌─────────────────────────────────────────────────────────────────────────┐
1585
- │ RESPONSE FLOW (←) │
1586
- │ │
1587
- │ ┌──────────────────────┐ │
1588
- │ │ Behavior A │ 4. Post-processing (after next() returns) │
1589
- │ │ (priority: -100) │ - Catch errors │
1590
- │ │ │ - Calculate duration │
1591
- │ │ ◄── result ─────────┼── │
1592
- │ └──────────────────────┘ │
1593
- │ │ ▲ │
1594
- │ ▼ │ │
1595
- │ Response ┌──────────────────────┐ │
1596
- │ │ Behavior B │ 3. Post-processing │
1597
- │ │ (priority: 0) │ - Log response │
1598
- │ │ │ - Log duration │
1599
- │ │ ◄── result ─────────┼── │
1600
- │ └──────────────────────┘ │
1601
- │ ▲ │
1602
- │ │ │
1603
- │ ┌──────────────────────┐ │
1604
- │ │ Behavior C │ │
1605
- │ │ (priority: 100) │ │
1606
- │ │ │ │
1607
- │ │ ◄── result ─────────┼──┤
1608
- │ └──────────────────────┘ │
1609
- │ │
1610
- └─────────────────────────────────────────────────────────────────────────┘
1611
- ```
1612
-
1613
- **Code Example:**
1614
-
1615
- ```typescript
1616
- @Injectable()
1617
- @PipelineBehavior({ priority: 0 })
1618
- export class LoggingBehavior<TRequest, TResponse>
1619
- implements IPipelineBehavior<TRequest, TResponse>
1620
- {
1621
- async handle(
1622
- request: TRequest,
1623
- next: () => Promise<TResponse> // Delegate to next behavior/handler
1624
- ): Promise<TResponse> {
1625
- // ═══════════════════════════════════════════
1626
- // PRE-PROCESSING (runs BEFORE the handler)
1627
- // ═══════════════════════════════════════════
1628
- const start = Date.now();
1629
- console.log(`→ Handling ${request.constructor.name}...`);
1630
-
1631
- // ═══════════════════════════════════════════
1632
- // CALL NEXT (invokes next behavior or handler)
1633
- // ═══════════════════════════════════════════
1634
- const response = await next();
1635
-
1636
- // ═══════════════════════════════════════════
1637
- // POST-PROCESSING (runs AFTER the handler)
1638
- // ═══════════════════════════════════════════
1639
- const duration = Date.now() - start;
1640
- console.log(`← Handled ${request.constructor.name} in ${duration}ms`);
1641
-
1642
- return response;
967
+ // Dedicated consumer for the compensating event
968
+ @EventHandler(InventoryReleasedEvent)
969
+ export class HandleInventoryReleasedHandler implements IEventConsumer<InventoryReleasedEvent> {
970
+ async handle(event: InventoryReleasedEvent): Promise<void> {
971
+ await this.inventoryService.release(event.orderId); // Logic lives here now
1643
972
  }
1644
973
  }
1645
974
  ```
1646
975
 
1647
- **Key Points:**
1648
-
1649
- 1. **`next()` is a function** that returns a `Promise` - you must `await` it
1650
- 2. **Code before `await next()`** runs during the request phase (pre-processing)
1651
- 3. **Code after `await next()`** runs during the response phase (post-processing)
1652
- 4. **Lower priority = outer wrapper** - executes first on request, last on response
1653
- 5. **Higher priority = inner wrapper** - executes last on request, first on response
1654
- 6. **If you don't call `next()`**, the handler never executes (useful for short-circuiting)
1655
- 7. **Exceptions propagate outward** through the `await next()` chain
1656
-
1657
- ### Pipeline Execution Order
1658
-
1659
- With behaviors at priorities -100, 0, 10, 100:
1660
-
1661
- ```
1662
- Request → ExceptionHandling(-100) → Logging(0) → Performance(10) → Validation(100) → Handler
1663
- Response ← ExceptionHandling(-100) ← Logging(0) ← Performance(10) ← Validation(100) ← Handler
1664
- ```
1665
-
1666
- ## Best Practices
1667
-
1668
- 1. **Keep Commands and Queries Simple**: They should be simple data containers with minimal logic.
1669
-
1670
- 2. **One Handler Per Command/Query**: Each command or query should have exactly one handler.
1671
-
1672
- 3. **Use Dependency Injection**: Inject required services into your handlers through the constructor.
1673
-
1674
- 4. **Type Safety**: Always specify the return type for queries using the generic parameters.
1675
-
1676
- 5. **Error Handling**: Implement proper error handling in your handlers.
976
+ ### Other Changes
1677
977
 
1678
- 6. **Validation**: Validate command/query data before creating instances or in the handler.
978
+ - **`IEventHandler` renamed to `IEventConsumer`** `IEventHandler` is a deprecated alias
979
+ - **MediatorBus API unchanged** — `send()`, `query()`, `publish()` work as before
1679
980
 
1680
981
  ## License
1681
982