@rolandsall24/nest-mediator 0.5.2 → 0.7.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.
- package/README.md +597 -40
- package/dist/lib/decorators/event-criticality.decorator.d.ts +61 -0
- package/dist/lib/decorators/event-criticality.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/event-criticality.decorator.js +71 -0
- package/dist/lib/decorators/event-criticality.decorator.js.map +1 -0
- package/dist/lib/decorators/event-handler.decorator.d.ts +21 -0
- package/dist/lib/decorators/event-handler.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/event-handler.decorator.js +27 -0
- package/dist/lib/decorators/event-handler.decorator.js.map +1 -0
- package/dist/lib/decorators/index.d.ts +2 -0
- package/dist/lib/decorators/index.d.ts.map +1 -1
- package/dist/lib/decorators/index.js +2 -0
- package/dist/lib/decorators/index.js.map +1 -1
- package/dist/lib/decorators/pipeline-behavior.decorator.d.ts +49 -0
- package/dist/lib/decorators/pipeline-behavior.decorator.d.ts.map +1 -1
- package/dist/lib/decorators/pipeline-behavior.decorator.js +56 -1
- package/dist/lib/decorators/pipeline-behavior.decorator.js.map +1 -1
- package/dist/lib/interfaces/command-bus.interface.d.ts +25 -0
- package/dist/lib/interfaces/command-bus.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/command-bus.interface.js +3 -0
- package/dist/lib/interfaces/command-bus.interface.js.map +1 -0
- package/dist/lib/interfaces/event-bus.interface.d.ts +35 -0
- package/dist/lib/interfaces/event-bus.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/event-bus.interface.js +3 -0
- package/dist/lib/interfaces/event-bus.interface.js.map +1 -0
- package/dist/lib/interfaces/event-consumer.interface.d.ts +64 -0
- package/dist/lib/interfaces/event-consumer.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/event-consumer.interface.js +3 -0
- package/dist/lib/interfaces/event-consumer.interface.js.map +1 -0
- package/dist/lib/interfaces/event-criticality.interface.d.ts +28 -0
- package/dist/lib/interfaces/event-criticality.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/event-criticality.interface.js +25 -0
- package/dist/lib/interfaces/event-criticality.interface.js.map +1 -0
- package/dist/lib/interfaces/event-handler.interface.d.ts +24 -0
- package/dist/lib/interfaces/event-handler.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/event-handler.interface.js +3 -0
- package/dist/lib/interfaces/event-handler.interface.js.map +1 -0
- package/dist/lib/interfaces/event.interface.d.ts +30 -0
- package/dist/lib/interfaces/event.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/event.interface.js +3 -0
- package/dist/lib/interfaces/event.interface.js.map +1 -0
- package/dist/lib/interfaces/index.d.ts +7 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -1
- package/dist/lib/interfaces/index.js +7 -0
- package/dist/lib/interfaces/index.js.map +1 -1
- package/dist/lib/interfaces/mediator.interface.d.ts +14 -0
- package/dist/lib/interfaces/mediator.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/mediator.interface.js +3 -0
- package/dist/lib/interfaces/mediator.interface.js.map +1 -0
- package/dist/lib/interfaces/query-bus.interface.d.ts +26 -0
- package/dist/lib/interfaces/query-bus.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/query-bus.interface.js +3 -0
- package/dist/lib/interfaces/query-bus.interface.js.map +1 -0
- package/dist/lib/nest-mediator.module.d.ts.map +1 -1
- package/dist/lib/nest-mediator.module.js +37 -2
- package/dist/lib/nest-mediator.module.js.map +1 -1
- package/dist/lib/services/command.bus.d.ts +30 -0
- package/dist/lib/services/command.bus.d.ts.map +1 -0
- package/dist/lib/services/command.bus.js +69 -0
- package/dist/lib/services/command.bus.js.map +1 -0
- package/dist/lib/services/event.bus.d.ts +61 -0
- package/dist/lib/services/event.bus.d.ts.map +1 -0
- package/dist/lib/services/event.bus.js +176 -0
- package/dist/lib/services/event.bus.js.map +1 -0
- package/dist/lib/services/mediator.bus.d.ts +50 -31
- package/dist/lib/services/mediator.bus.d.ts.map +1 -1
- package/dist/lib/services/mediator.bus.js +60 -97
- package/dist/lib/services/mediator.bus.js.map +1 -1
- package/dist/lib/services/pipeline.orchestrator.d.ts +46 -0
- package/dist/lib/services/pipeline.orchestrator.d.ts.map +1 -0
- package/dist/lib/services/pipeline.orchestrator.js +87 -0
- package/dist/lib/services/pipeline.orchestrator.js.map +1 -0
- package/dist/lib/services/query.bus.d.ts +31 -0
- package/dist/lib/services/query.bus.d.ts.map +1 -0
- package/dist/lib/services/query.bus.js +68 -0
- package/dist/lib/services/query.bus.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,13 @@ A lightweight CQRS (Command Query Responsibility Segregation) mediator pattern i
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Clean separation between Commands and
|
|
7
|
+
- Clean separation between Commands, Queries, and Events
|
|
8
8
|
- Type-safe handlers with TypeScript
|
|
9
9
|
- Decorator-based handler registration
|
|
10
10
|
- Automatic handler discovery and registration
|
|
11
|
-
-
|
|
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+)**
|
|
12
14
|
- Built-in behaviors: Logging, Validation, Exception Handling, Performance Tracking
|
|
13
15
|
- Built on top of NestJS dependency injection
|
|
14
16
|
- Zero runtime dependencies beyond NestJS
|
|
@@ -32,57 +34,80 @@ This library requires TypeScript decorators to be enabled. Add the following to
|
|
|
32
34
|
}
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
## Upgrading to v0.
|
|
37
|
+
## Upgrading to v0.7.0
|
|
36
38
|
|
|
37
|
-
Version 0.
|
|
39
|
+
Version 0.7.0 introduces **Domain Events** with Critical and Non-Critical consumer support.
|
|
38
40
|
|
|
39
41
|
### What's New
|
|
40
42
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
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)
|
|
45
53
|
|
|
46
|
-
###
|
|
54
|
+
### Backward Compatibility
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
- **`IEventHandler` renamed to `IEventConsumer`**: `IEventHandler` is now a deprecated type alias. Update your imports:
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
68
|
+
|
|
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
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
import { HandlerNotFoundException } from '@rolandsall24/nest-mediator';
|
|
73
|
+
### Type-Specific Behavior Example
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
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();
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
```
|
|
71
99
|
|
|
72
|
-
###
|
|
100
|
+
### How It Works
|
|
73
101
|
|
|
74
|
-
|
|
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
|
|
75
105
|
|
|
76
|
-
|
|
77
|
-
// Existing code - works exactly as before (no behaviors)
|
|
78
|
-
NestMediatorModule.forRoot()
|
|
106
|
+
### No Migration Required
|
|
79
107
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
enableValidation: true,
|
|
84
|
-
})
|
|
85
|
-
```
|
|
108
|
+
Existing behaviors without `@Handle()` on the method continue to work exactly as before - they apply to all requests matching their scope.
|
|
109
|
+
|
|
110
|
+
---
|
|
86
111
|
|
|
87
112
|
## Quick Start
|
|
88
113
|
|
|
@@ -287,6 +312,236 @@ export class UserController {
|
|
|
287
312
|
}
|
|
288
313
|
```
|
|
289
314
|
|
|
315
|
+
### Domain Events
|
|
316
|
+
|
|
317
|
+
Domain events notify other parts of the system when something important happens. They support two consumer types:
|
|
318
|
+
|
|
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.
|
|
321
|
+
|
|
322
|
+
#### 1. Define an Event
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { IEvent } from '@rolandsall24/nest-mediator';
|
|
326
|
+
|
|
327
|
+
export class OrderPlacedEvent implements IEvent {
|
|
328
|
+
constructor(
|
|
329
|
+
public readonly orderId: string,
|
|
330
|
+
public readonly customerId: string,
|
|
331
|
+
public readonly items: { productId: string; quantity: number }[],
|
|
332
|
+
public readonly total: number,
|
|
333
|
+
) {}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### 2. Create Event Consumers
|
|
338
|
+
|
|
339
|
+
**Critical consumer** (must succeed, runs in order):
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
343
|
+
import { EventHandler, IEventConsumer, Critical } from '@rolandsall24/nest-mediator';
|
|
344
|
+
import { OrderPlacedEvent } from './order-placed.event';
|
|
345
|
+
|
|
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);
|
|
351
|
+
|
|
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
|
+
```
|
|
359
|
+
|
|
360
|
+
**Critical consumer with compensation** (implements rollback on failure):
|
|
361
|
+
|
|
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';
|
|
366
|
+
|
|
367
|
+
@Injectable()
|
|
368
|
+
@EventHandler(OrderPlacedEvent)
|
|
369
|
+
@Critical({ order: 2 })
|
|
370
|
+
export class ReserveInventoryConsumer implements ICriticalEventConsumer<OrderPlacedEvent> {
|
|
371
|
+
private readonly logger = new Logger(ReserveInventoryConsumer.name);
|
|
372
|
+
|
|
373
|
+
async handle(event: OrderPlacedEvent): Promise<void> {
|
|
374
|
+
this.logger.log(`Reserving inventory for order ${event.orderId}`);
|
|
375
|
+
// Reserve inventory in the database
|
|
376
|
+
}
|
|
377
|
+
|
|
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
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Non-critical consumer** (fire-and-forget):
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
390
|
+
import { EventHandler, IEventConsumer, NonCritical } from '@rolandsall24/nest-mediator';
|
|
391
|
+
import { OrderPlacedEvent } from './order-placed.event';
|
|
392
|
+
|
|
393
|
+
@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);
|
|
398
|
+
|
|
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
|
+
}
|
|
404
|
+
|
|
405
|
+
// Consumers without @Critical or @NonCritical default to non-critical
|
|
406
|
+
@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
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
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:
|
|
418
|
+
|
|
419
|
+
```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
|
+
@Injectable()
|
|
426
|
+
@CommandHandler(PlaceOrderCommand)
|
|
427
|
+
export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
|
|
428
|
+
private readonly logger = new Logger(PlaceOrderHandler.name);
|
|
429
|
+
|
|
430
|
+
constructor(private readonly mediatorBus: MediatorBus) {}
|
|
431
|
+
|
|
432
|
+
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
|
+
),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
this.logger.log(
|
|
451
|
+
`Order ${orderId} completed. Critical: ${result.criticalSucceeded}, Non-critical dispatched: ${result.nonCriticalDispatched}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private async processOrder(orderId: string, command: PlaceOrderCommand): Promise<void> {
|
|
456
|
+
// Order processing logic here
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### 4. Event Execution Flow
|
|
462
|
+
|
|
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
|
|
481
|
+
|
|
482
|
+
Only runs if ALL critical consumers succeed
|
|
483
|
+
Failures logged but don't affect the result
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### 5. Compensation Pattern (Saga)
|
|
487
|
+
|
|
488
|
+
Critical consumers can implement the `ICriticalEventConsumer` interface with an optional `compensate()` method to support saga-style rollback:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { ICriticalEventConsumer, EventHandler, Critical } from '@rolandsall24/nest-mediator';
|
|
492
|
+
|
|
493
|
+
@Injectable()
|
|
494
|
+
@EventHandler(OrderPlacedEvent)
|
|
495
|
+
@Critical({ order: 3 })
|
|
496
|
+
export class CreateOrderRecordConsumer implements ICriticalEventConsumer<OrderPlacedEvent> {
|
|
497
|
+
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
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async compensate(event: OrderPlacedEvent): Promise<void> {
|
|
508
|
+
// Rollback: delete the order record
|
|
509
|
+
await this.orderRepository.delete(event.orderId);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
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`:
|
|
525
|
+
|
|
526
|
+
```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 {}
|
|
543
|
+
```
|
|
544
|
+
|
|
290
545
|
## Complete Example
|
|
291
546
|
|
|
292
547
|
Here's a complete example following Domain-Driven Design principles with proper separation of concerns:
|
|
@@ -682,6 +937,63 @@ export interface IPipelineBehavior<TRequest = any, TResponse = any> {
|
|
|
682
937
|
}
|
|
683
938
|
```
|
|
684
939
|
|
|
940
|
+
#### `IEvent`
|
|
941
|
+
|
|
942
|
+
Marker interface for domain events.
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
export interface IEvent {}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
#### `IEventConsumer<TEvent>`
|
|
949
|
+
|
|
950
|
+
Interface for event consumers (non-critical or critical without compensation).
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
export interface IEventConsumer<TEvent extends IEvent> {
|
|
954
|
+
handle(event: TEvent): Promise<void>;
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
#### `ICriticalEventConsumer<TEvent>`
|
|
959
|
+
|
|
960
|
+
Interface for critical event consumers with optional compensation support (saga pattern).
|
|
961
|
+
|
|
962
|
+
```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>;
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
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
|
|
983
|
+
}
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
#### `EventCriticality`
|
|
987
|
+
|
|
988
|
+
Enum for event consumer criticality.
|
|
989
|
+
|
|
990
|
+
```typescript
|
|
991
|
+
export enum EventCriticality {
|
|
992
|
+
CRITICAL = 'critical',
|
|
993
|
+
NON_CRITICAL = 'non-critical',
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
685
997
|
### Decorators
|
|
686
998
|
|
|
687
999
|
#### `@CommandHandler(command)`
|
|
@@ -707,6 +1019,38 @@ Marks a class as a pipeline behavior.
|
|
|
707
1019
|
- `options.scope` - `'command'`, `'query'`, or `'all'` (default: `'all'`)
|
|
708
1020
|
- **Usage**: Apply to behavior classes that implement `IPipelineBehavior`
|
|
709
1021
|
|
|
1022
|
+
#### `@Handle()`
|
|
1023
|
+
|
|
1024
|
+
Method decorator that enables automatic request type inference for pipeline behaviors.
|
|
1025
|
+
|
|
1026
|
+
- **Parameters**: None
|
|
1027
|
+
- **Usage**: Apply to the `handle` method to make the behavior type-specific
|
|
1028
|
+
|
|
1029
|
+
```typescript
|
|
1030
|
+
// Generic behavior - applies to ALL requests in scope
|
|
1031
|
+
@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
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
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
|
+
|
|
710
1054
|
#### `@SkipBehavior(behavior | behaviors[])`
|
|
711
1055
|
|
|
712
1056
|
Excludes specific pipeline behaviors from a command or query.
|
|
@@ -717,11 +1061,36 @@ Excludes specific pipeline behaviors from a command or query.
|
|
|
717
1061
|
- **Usage**: Apply to command or query classes
|
|
718
1062
|
- **Works with**: Both built-in behaviors and custom behaviors
|
|
719
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
|
+
|
|
720
1089
|
### Services
|
|
721
1090
|
|
|
722
1091
|
#### `MediatorBus`
|
|
723
1092
|
|
|
724
|
-
The main service for sending commands and
|
|
1093
|
+
The main service for sending commands, queries, and events.
|
|
725
1094
|
|
|
726
1095
|
##### Methods
|
|
727
1096
|
|
|
@@ -731,7 +1100,7 @@ Sends a command to its registered handler.
|
|
|
731
1100
|
|
|
732
1101
|
- **Parameters**: `command` - The command instance to execute
|
|
733
1102
|
- **Returns**: Promise that resolves when the command is executed
|
|
734
|
-
- **Throws**:
|
|
1103
|
+
- **Throws**: `HandlerNotFoundException` if no handler is registered for the command
|
|
735
1104
|
|
|
736
1105
|
**`query<TQuery, TResult>(query: TQuery): Promise<TResult>`**
|
|
737
1106
|
|
|
@@ -739,7 +1108,18 @@ Executes a query through its registered handler.
|
|
|
739
1108
|
|
|
740
1109
|
- **Parameters**: `query` - The query instance to execute
|
|
741
1110
|
- **Returns**: Promise that resolves with the query result
|
|
742
|
-
- **Throws**:
|
|
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
|
|
743
1123
|
|
|
744
1124
|
### Module Configuration
|
|
745
1125
|
|
|
@@ -1044,6 +1424,65 @@ export class AuthorizationBehavior<TRequest, TResponse>
|
|
|
1044
1424
|
}
|
|
1045
1425
|
```
|
|
1046
1426
|
|
|
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:
|
|
1430
|
+
|
|
1431
|
+
```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
|
+
@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
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
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`)
|
|
1477
|
+
|
|
1478
|
+
**Comparison:**
|
|
1479
|
+
|
|
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>` |
|
|
1485
|
+
|
|
1047
1486
|
### Complete Behavior Execution Order Example
|
|
1048
1487
|
|
|
1049
1488
|
With the following behaviors configured:
|
|
@@ -1097,6 +1536,124 @@ export class CreateUserCommand implements ICommand {
|
|
|
1097
1536
|
// If validation fails, ValidationException is thrown with details
|
|
1098
1537
|
```
|
|
1099
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;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
```
|
|
1646
|
+
|
|
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
|
+
|
|
1100
1657
|
### Pipeline Execution Order
|
|
1101
1658
|
|
|
1102
1659
|
With behaviors at priorities -100, 0, 10, 100:
|