@rolandsall24/nest-mediator 0.6.0 → 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 +354 -56
- 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/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 +19 -0
- 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 +48 -30
- package/dist/lib/services/mediator.bus.d.ts.map +1 -1
- package/dist/lib/services/mediator.bus.js +58 -101
- 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,12 +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
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+)**
|
|
13
14
|
- Built-in behaviors: Logging, Validation, Exception Handling, Performance Tracking
|
|
14
15
|
- Built on top of NestJS dependency injection
|
|
15
16
|
- Zero runtime dependencies beyond NestJS
|
|
@@ -33,6 +34,32 @@ This library requires TypeScript decorators to be enabled. Add the following to
|
|
|
33
34
|
}
|
|
34
35
|
```
|
|
35
36
|
|
|
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
|
+
|
|
36
63
|
## Upgrading to v0.6.0
|
|
37
64
|
|
|
38
65
|
Version 0.6.0 introduces **Type-Specific Pipeline Behaviors** - behaviors that only apply to specific request types.
|
|
@@ -82,58 +109,6 @@ Existing behaviors without `@Handle()` on the method continue to work exactly as
|
|
|
82
109
|
|
|
83
110
|
---
|
|
84
111
|
|
|
85
|
-
## Upgrading to v0.5.0
|
|
86
|
-
|
|
87
|
-
Version 0.5.0 introduces **Pipeline Behaviors** while maintaining backward compatibility. Existing code using `NestMediatorModule.forRoot()` will continue to work without changes.
|
|
88
|
-
|
|
89
|
-
### What's New
|
|
90
|
-
|
|
91
|
-
- Pipeline behaviors for cross-cutting concerns (logging, validation, etc.)
|
|
92
|
-
- Built-in behaviors: `LoggingBehavior`, `ValidationBehavior`, `ExceptionHandlingBehavior`, `PerformanceBehavior`
|
|
93
|
-
- New `forRoot()` method for enabling behaviors
|
|
94
|
-
- Custom `HandlerNotFoundException` for better error handling
|
|
95
|
-
|
|
96
|
-
### Breaking Change Notice
|
|
97
|
-
|
|
98
|
-
The `send()` and `query()` methods now throw `HandlerNotFoundException` instead of a generic `Error` when no handler is registered. This is **backward compatible** since `HandlerNotFoundException` extends `Error`, but you may want to update your error handling for more specific catches:
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
// Before (still works)
|
|
102
|
-
try {
|
|
103
|
-
await mediator.send(command);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (error instanceof Error) {
|
|
106
|
-
console.log(error.message); // Works as before
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// After (optional - for more specific handling)
|
|
111
|
-
import { HandlerNotFoundException } from '@rolandsall24/nest-mediator';
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await mediator.send(command);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error instanceof HandlerNotFoundException) {
|
|
117
|
-
console.log(`No handler for: ${error.requestName}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### No Migration Required
|
|
123
|
-
|
|
124
|
-
If you're using `NestMediatorModule.forRoot()`, no changes are needed. Pipeline behaviors are **opt-in** via `forRoot()`:
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
// Existing code - works exactly as before (no behaviors)
|
|
128
|
-
NestMediatorModule.forRoot()
|
|
129
|
-
|
|
130
|
-
// New - opt-in to behaviors
|
|
131
|
-
NestMediatorModule.forRoot({
|
|
132
|
-
enableLogging: true,
|
|
133
|
-
enableValidation: true,
|
|
134
|
-
})
|
|
135
|
-
```
|
|
136
|
-
|
|
137
112
|
## Quick Start
|
|
138
113
|
|
|
139
114
|
### 1. Import the Module
|
|
@@ -337,6 +312,236 @@ export class UserController {
|
|
|
337
312
|
}
|
|
338
313
|
```
|
|
339
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
|
+
|
|
340
545
|
## Complete Example
|
|
341
546
|
|
|
342
547
|
Here's a complete example following Domain-Driven Design principles with proper separation of concerns:
|
|
@@ -732,6 +937,63 @@ export interface IPipelineBehavior<TRequest = any, TResponse = any> {
|
|
|
732
937
|
}
|
|
733
938
|
```
|
|
734
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
|
+
|
|
735
997
|
### Decorators
|
|
736
998
|
|
|
737
999
|
#### `@CommandHandler(command)`
|
|
@@ -799,11 +1061,36 @@ Excludes specific pipeline behaviors from a command or query.
|
|
|
799
1061
|
- **Usage**: Apply to command or query classes
|
|
800
1062
|
- **Works with**: Both built-in behaviors and custom behaviors
|
|
801
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
|
+
|
|
802
1089
|
### Services
|
|
803
1090
|
|
|
804
1091
|
#### `MediatorBus`
|
|
805
1092
|
|
|
806
|
-
The main service for sending commands and
|
|
1093
|
+
The main service for sending commands, queries, and events.
|
|
807
1094
|
|
|
808
1095
|
##### Methods
|
|
809
1096
|
|
|
@@ -813,7 +1100,7 @@ Sends a command to its registered handler.
|
|
|
813
1100
|
|
|
814
1101
|
- **Parameters**: `command` - The command instance to execute
|
|
815
1102
|
- **Returns**: Promise that resolves when the command is executed
|
|
816
|
-
- **Throws**:
|
|
1103
|
+
- **Throws**: `HandlerNotFoundException` if no handler is registered for the command
|
|
817
1104
|
|
|
818
1105
|
**`query<TQuery, TResult>(query: TQuery): Promise<TResult>`**
|
|
819
1106
|
|
|
@@ -821,7 +1108,18 @@ Executes a query through its registered handler.
|
|
|
821
1108
|
|
|
822
1109
|
- **Parameters**: `query` - The query instance to execute
|
|
823
1110
|
- **Returns**: Promise that resolves with the query result
|
|
824
|
-
- **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
|
|
825
1123
|
|
|
826
1124
|
### Module Configuration
|
|
827
1125
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export { EventCriticality, EventCriticalityMetadata } from '../interfaces/event-criticality.interface.js';
|
|
2
|
+
export declare const EVENT_CRITICALITY_METADATA = "EVENT_CRITICALITY_METADATA";
|
|
3
|
+
/**
|
|
4
|
+
* Options for critical event handlers
|
|
5
|
+
*/
|
|
6
|
+
export interface CriticalOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Execution order within critical handlers.
|
|
9
|
+
* Lower numbers execute first.
|
|
10
|
+
* Default: 0
|
|
11
|
+
*/
|
|
12
|
+
order?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Decorator to mark an event handler as critical.
|
|
16
|
+
* Critical handlers:
|
|
17
|
+
* - Run sequentially in the order specified
|
|
18
|
+
* - Must complete before non-critical handlers start
|
|
19
|
+
* - If one fails, the publish operation fails (remaining critical handlers are skipped)
|
|
20
|
+
* - Are awaited by the caller
|
|
21
|
+
*
|
|
22
|
+
* @param options - Optional configuration (order)
|
|
23
|
+
* @returns Class decorator
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* @EventHandler(OrderPlacedEvent)
|
|
28
|
+
* @Critical({ order: 1 })
|
|
29
|
+
* export class ReserveInventoryHandler implements IEventConsumer<OrderPlacedEvent> {
|
|
30
|
+
* async handle(event: OrderPlacedEvent): Promise<void> {
|
|
31
|
+
* await this.inventoryService.reserve(event.items);
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const Critical: (options?: CriticalOptions) => ClassDecorator;
|
|
37
|
+
/**
|
|
38
|
+
* Decorator to explicitly mark an event handler as non-critical.
|
|
39
|
+
* Non-critical handlers:
|
|
40
|
+
* - Run in parallel after all critical handlers complete
|
|
41
|
+
* - Fire and forget (not awaited by the caller)
|
|
42
|
+
* - Failures are logged but don't affect the publish result
|
|
43
|
+
* - Don't block the caller
|
|
44
|
+
*
|
|
45
|
+
* Note: Handlers without @Critical or @NonCritical are non-critical by default.
|
|
46
|
+
*
|
|
47
|
+
* @returns Class decorator
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* @EventHandler(OrderPlacedEvent)
|
|
52
|
+
* @NonCritical()
|
|
53
|
+
* export class SendOrderConfirmationEmail implements IEventConsumer<OrderPlacedEvent> {
|
|
54
|
+
* async handle(event: OrderPlacedEvent): Promise<void> {
|
|
55
|
+
* await this.emailService.send(event.customerEmail, 'order_confirmed');
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare const NonCritical: () => ClassDecorator;
|
|
61
|
+
//# sourceMappingURL=event-criticality.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-criticality.decorator.d.ts","sourceRoot":"","sources":["../../../src/lib/decorators/event-criticality.decorator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AAE1G,eAAO,MAAM,0BAA0B,+BAA+B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,QAAQ,GAAI,UAAS,eAAoB,KAAG,cAMxD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,QAAO,cAM9B,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NonCritical = exports.Critical = exports.EVENT_CRITICALITY_METADATA = exports.EventCriticality = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const event_criticality_interface_js_1 = require("../interfaces/event-criticality.interface.js");
|
|
6
|
+
// Re-export types for backward compatibility
|
|
7
|
+
var event_criticality_interface_js_2 = require("../interfaces/event-criticality.interface.js");
|
|
8
|
+
Object.defineProperty(exports, "EventCriticality", { enumerable: true, get: function () { return event_criticality_interface_js_2.EventCriticality; } });
|
|
9
|
+
exports.EVENT_CRITICALITY_METADATA = 'EVENT_CRITICALITY_METADATA';
|
|
10
|
+
/**
|
|
11
|
+
* Decorator to mark an event handler as critical.
|
|
12
|
+
* Critical handlers:
|
|
13
|
+
* - Run sequentially in the order specified
|
|
14
|
+
* - Must complete before non-critical handlers start
|
|
15
|
+
* - If one fails, the publish operation fails (remaining critical handlers are skipped)
|
|
16
|
+
* - Are awaited by the caller
|
|
17
|
+
*
|
|
18
|
+
* @param options - Optional configuration (order)
|
|
19
|
+
* @returns Class decorator
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* @EventHandler(OrderPlacedEvent)
|
|
24
|
+
* @Critical({ order: 1 })
|
|
25
|
+
* export class ReserveInventoryHandler implements IEventConsumer<OrderPlacedEvent> {
|
|
26
|
+
* async handle(event: OrderPlacedEvent): Promise<void> {
|
|
27
|
+
* await this.inventoryService.reserve(event.items);
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
const Critical = (options = {}) => {
|
|
33
|
+
const metadata = {
|
|
34
|
+
criticality: event_criticality_interface_js_1.EventCriticality.CRITICAL,
|
|
35
|
+
order: options.order ?? 0,
|
|
36
|
+
};
|
|
37
|
+
return (0, common_1.SetMetadata)(exports.EVENT_CRITICALITY_METADATA, metadata);
|
|
38
|
+
};
|
|
39
|
+
exports.Critical = Critical;
|
|
40
|
+
/**
|
|
41
|
+
* Decorator to explicitly mark an event handler as non-critical.
|
|
42
|
+
* Non-critical handlers:
|
|
43
|
+
* - Run in parallel after all critical handlers complete
|
|
44
|
+
* - Fire and forget (not awaited by the caller)
|
|
45
|
+
* - Failures are logged but don't affect the publish result
|
|
46
|
+
* - Don't block the caller
|
|
47
|
+
*
|
|
48
|
+
* Note: Handlers without @Critical or @NonCritical are non-critical by default.
|
|
49
|
+
*
|
|
50
|
+
* @returns Class decorator
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* @EventHandler(OrderPlacedEvent)
|
|
55
|
+
* @NonCritical()
|
|
56
|
+
* export class SendOrderConfirmationEmail implements IEventConsumer<OrderPlacedEvent> {
|
|
57
|
+
* async handle(event: OrderPlacedEvent): Promise<void> {
|
|
58
|
+
* await this.emailService.send(event.customerEmail, 'order_confirmed');
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
const NonCritical = () => {
|
|
64
|
+
const metadata = {
|
|
65
|
+
criticality: event_criticality_interface_js_1.EventCriticality.NON_CRITICAL,
|
|
66
|
+
order: 0,
|
|
67
|
+
};
|
|
68
|
+
return (0, common_1.SetMetadata)(exports.EVENT_CRITICALITY_METADATA, metadata);
|
|
69
|
+
};
|
|
70
|
+
exports.NonCritical = NonCritical;
|
|
71
|
+
//# sourceMappingURL=event-criticality.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-criticality.decorator.js","sourceRoot":"","sources":["../../../src/lib/decorators/event-criticality.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAC7C,iGAGsD;AAEtD,6CAA6C;AAC7C,+FAA0G;AAAjG,kIAAA,gBAAgB,OAAA;AAEZ,QAAA,0BAA0B,GAAG,4BAA4B,CAAC;AAcvE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,MAAM,QAAQ,GAAG,CAAC,UAA2B,EAAE,EAAkB,EAAE;IACxE,MAAM,QAAQ,GAA6B;QACzC,WAAW,EAAE,iDAAgB,CAAC,QAAQ;QACtC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAC1B,CAAC;IACF,OAAO,IAAA,oBAAW,EAAC,kCAA0B,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC,CAAC;AANW,QAAA,QAAQ,YAMnB;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACI,MAAM,WAAW,GAAG,GAAmB,EAAE;IAC9C,MAAM,QAAQ,GAA6B;QACzC,WAAW,EAAE,iDAAgB,CAAC,YAAY;QAC1C,KAAK,EAAE,CAAC;KACT,CAAC;IACF,OAAO,IAAA,oBAAW,EAAC,kCAA0B,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC,CAAC;AANW,QAAA,WAAW,eAMtB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IEvent } from '../interfaces/index.js';
|
|
2
|
+
export declare const EVENT_HANDLER_METADATA = "EVENT_HANDLER_METADATA";
|
|
3
|
+
/**
|
|
4
|
+
* Decorator to mark a class as an event consumer.
|
|
5
|
+
* Multiple consumers can subscribe to the same event.
|
|
6
|
+
*
|
|
7
|
+
* @param event - The event class that this consumer handles
|
|
8
|
+
* @returns Class decorator
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @EventHandler(UserCreatedEvent)
|
|
13
|
+
* export class SendWelcomeEmailConsumer implements IEventConsumer<UserCreatedEvent> {
|
|
14
|
+
* async handle(event: UserCreatedEvent): Promise<void> {
|
|
15
|
+
* await this.emailService.sendWelcome(event.userEmail);
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const EventHandler: (event: new (...args: any[]) => IEvent) => ClassDecorator;
|
|
21
|
+
//# sourceMappingURL=event-handler.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-handler.decorator.d.ts","sourceRoot":"","sources":["../../../src/lib/decorators/event-handler.decorator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEhD,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,KACpC,cAEF,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventHandler = exports.EVENT_HANDLER_METADATA = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
exports.EVENT_HANDLER_METADATA = 'EVENT_HANDLER_METADATA';
|
|
6
|
+
/**
|
|
7
|
+
* Decorator to mark a class as an event consumer.
|
|
8
|
+
* Multiple consumers can subscribe to the same event.
|
|
9
|
+
*
|
|
10
|
+
* @param event - The event class that this consumer handles
|
|
11
|
+
* @returns Class decorator
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* @EventHandler(UserCreatedEvent)
|
|
16
|
+
* export class SendWelcomeEmailConsumer implements IEventConsumer<UserCreatedEvent> {
|
|
17
|
+
* async handle(event: UserCreatedEvent): Promise<void> {
|
|
18
|
+
* await this.emailService.sendWelcome(event.userEmail);
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
const EventHandler = (event) => {
|
|
24
|
+
return (0, common_1.SetMetadata)(exports.EVENT_HANDLER_METADATA, event);
|
|
25
|
+
};
|
|
26
|
+
exports.EventHandler = EventHandler;
|
|
27
|
+
//# sourceMappingURL=event-handler.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-handler.decorator.js","sourceRoot":"","sources":["../../../src/lib/decorators/event-handler.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAGhC,QAAA,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D;;;;;;;;;;;;;;;;GAgBG;AACI,MAAM,YAAY,GAAG,CAC1B,KAAqC,EACrB,EAAE;IAClB,OAAO,IAAA,oBAAW,EAAC,8BAAsB,EAAE,KAAK,CAAC,CAAC;AACpD,CAAC,CAAC;AAJW,QAAA,YAAY,gBAIvB"}
|
|
@@ -2,4 +2,6 @@ export * from './command-handler.decorator.js';
|
|
|
2
2
|
export * from './query-handler.decorator.js';
|
|
3
3
|
export * from './pipeline-behavior.decorator.js';
|
|
4
4
|
export * from './skip-behavior.decorator.js';
|
|
5
|
+
export * from './event-handler.decorator.js';
|
|
6
|
+
export * from './event-criticality.decorator.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/decorators/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/decorators/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC"}
|
|
@@ -18,4 +18,6 @@ __exportStar(require("./command-handler.decorator.js"), exports);
|
|
|
18
18
|
__exportStar(require("./query-handler.decorator.js"), exports);
|
|
19
19
|
__exportStar(require("./pipeline-behavior.decorator.js"), exports);
|
|
20
20
|
__exportStar(require("./skip-behavior.decorator.js"), exports);
|
|
21
|
+
__exportStar(require("./event-handler.decorator.js"), exports);
|
|
22
|
+
__exportStar(require("./event-criticality.decorator.js"), exports);
|
|
21
23
|
//# sourceMappingURL=index.js.map
|