@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.
Files changed (77) hide show
  1. package/README.md +597 -40
  2. package/dist/lib/decorators/event-criticality.decorator.d.ts +61 -0
  3. package/dist/lib/decorators/event-criticality.decorator.d.ts.map +1 -0
  4. package/dist/lib/decorators/event-criticality.decorator.js +71 -0
  5. package/dist/lib/decorators/event-criticality.decorator.js.map +1 -0
  6. package/dist/lib/decorators/event-handler.decorator.d.ts +21 -0
  7. package/dist/lib/decorators/event-handler.decorator.d.ts.map +1 -0
  8. package/dist/lib/decorators/event-handler.decorator.js +27 -0
  9. package/dist/lib/decorators/event-handler.decorator.js.map +1 -0
  10. package/dist/lib/decorators/index.d.ts +2 -0
  11. package/dist/lib/decorators/index.d.ts.map +1 -1
  12. package/dist/lib/decorators/index.js +2 -0
  13. package/dist/lib/decorators/index.js.map +1 -1
  14. package/dist/lib/decorators/pipeline-behavior.decorator.d.ts +49 -0
  15. package/dist/lib/decorators/pipeline-behavior.decorator.d.ts.map +1 -1
  16. package/dist/lib/decorators/pipeline-behavior.decorator.js +56 -1
  17. package/dist/lib/decorators/pipeline-behavior.decorator.js.map +1 -1
  18. package/dist/lib/interfaces/command-bus.interface.d.ts +25 -0
  19. package/dist/lib/interfaces/command-bus.interface.d.ts.map +1 -0
  20. package/dist/lib/interfaces/command-bus.interface.js +3 -0
  21. package/dist/lib/interfaces/command-bus.interface.js.map +1 -0
  22. package/dist/lib/interfaces/event-bus.interface.d.ts +35 -0
  23. package/dist/lib/interfaces/event-bus.interface.d.ts.map +1 -0
  24. package/dist/lib/interfaces/event-bus.interface.js +3 -0
  25. package/dist/lib/interfaces/event-bus.interface.js.map +1 -0
  26. package/dist/lib/interfaces/event-consumer.interface.d.ts +64 -0
  27. package/dist/lib/interfaces/event-consumer.interface.d.ts.map +1 -0
  28. package/dist/lib/interfaces/event-consumer.interface.js +3 -0
  29. package/dist/lib/interfaces/event-consumer.interface.js.map +1 -0
  30. package/dist/lib/interfaces/event-criticality.interface.d.ts +28 -0
  31. package/dist/lib/interfaces/event-criticality.interface.d.ts.map +1 -0
  32. package/dist/lib/interfaces/event-criticality.interface.js +25 -0
  33. package/dist/lib/interfaces/event-criticality.interface.js.map +1 -0
  34. package/dist/lib/interfaces/event-handler.interface.d.ts +24 -0
  35. package/dist/lib/interfaces/event-handler.interface.d.ts.map +1 -0
  36. package/dist/lib/interfaces/event-handler.interface.js +3 -0
  37. package/dist/lib/interfaces/event-handler.interface.js.map +1 -0
  38. package/dist/lib/interfaces/event.interface.d.ts +30 -0
  39. package/dist/lib/interfaces/event.interface.d.ts.map +1 -0
  40. package/dist/lib/interfaces/event.interface.js +3 -0
  41. package/dist/lib/interfaces/event.interface.js.map +1 -0
  42. package/dist/lib/interfaces/index.d.ts +7 -0
  43. package/dist/lib/interfaces/index.d.ts.map +1 -1
  44. package/dist/lib/interfaces/index.js +7 -0
  45. package/dist/lib/interfaces/index.js.map +1 -1
  46. package/dist/lib/interfaces/mediator.interface.d.ts +14 -0
  47. package/dist/lib/interfaces/mediator.interface.d.ts.map +1 -0
  48. package/dist/lib/interfaces/mediator.interface.js +3 -0
  49. package/dist/lib/interfaces/mediator.interface.js.map +1 -0
  50. package/dist/lib/interfaces/query-bus.interface.d.ts +26 -0
  51. package/dist/lib/interfaces/query-bus.interface.d.ts.map +1 -0
  52. package/dist/lib/interfaces/query-bus.interface.js +3 -0
  53. package/dist/lib/interfaces/query-bus.interface.js.map +1 -0
  54. package/dist/lib/nest-mediator.module.d.ts.map +1 -1
  55. package/dist/lib/nest-mediator.module.js +37 -2
  56. package/dist/lib/nest-mediator.module.js.map +1 -1
  57. package/dist/lib/services/command.bus.d.ts +30 -0
  58. package/dist/lib/services/command.bus.d.ts.map +1 -0
  59. package/dist/lib/services/command.bus.js +69 -0
  60. package/dist/lib/services/command.bus.js.map +1 -0
  61. package/dist/lib/services/event.bus.d.ts +61 -0
  62. package/dist/lib/services/event.bus.d.ts.map +1 -0
  63. package/dist/lib/services/event.bus.js +176 -0
  64. package/dist/lib/services/event.bus.js.map +1 -0
  65. package/dist/lib/services/mediator.bus.d.ts +50 -31
  66. package/dist/lib/services/mediator.bus.d.ts.map +1 -1
  67. package/dist/lib/services/mediator.bus.js +60 -97
  68. package/dist/lib/services/mediator.bus.js.map +1 -1
  69. package/dist/lib/services/pipeline.orchestrator.d.ts +46 -0
  70. package/dist/lib/services/pipeline.orchestrator.d.ts.map +1 -0
  71. package/dist/lib/services/pipeline.orchestrator.js +87 -0
  72. package/dist/lib/services/pipeline.orchestrator.js.map +1 -0
  73. package/dist/lib/services/query.bus.d.ts +31 -0
  74. package/dist/lib/services/query.bus.d.ts.map +1 -0
  75. package/dist/lib/services/query.bus.js +68 -0
  76. package/dist/lib/services/query.bus.js.map +1 -0
  77. 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 Queries
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
- - **Pipeline Behaviors** for cross-cutting concerns (logging, validation, etc.)
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.5.0
37
+ ## Upgrading to v0.7.0
36
38
 
37
- Version 0.5.0 introduces **Pipeline Behaviors** while maintaining backward compatibility. Existing code using `NestMediatorModule.forRoot()` will continue to work without changes.
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
- - Pipeline behaviors for cross-cutting concerns (logging, validation, etc.)
42
- - Built-in behaviors: `LoggingBehavior`, `ValidationBehavior`, `ExceptionHandlingBehavior`, `PerformanceBehavior`
43
- - New `forRoot()` method for enabling behaviors
44
- - Custom `HandlerNotFoundException` for better error handling
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
- ### Breaking Change Notice
54
+ ### Backward Compatibility
47
55
 
48
- 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:
56
+ - **`IEventHandler` renamed to `IEventConsumer`**: `IEventHandler` is now a deprecated type alias. Update your imports:
49
57
 
50
- ```typescript
51
- // Before (still works)
52
- try {
53
- await mediator.send(command);
54
- } catch (error) {
55
- if (error instanceof Error) {
56
- console.log(error.message); // Works as before
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
- // After (optional - for more specific handling)
61
- import { HandlerNotFoundException } from '@rolandsall24/nest-mediator';
73
+ ### Type-Specific Behavior Example
62
74
 
63
- try {
64
- await mediator.send(command);
65
- } catch (error) {
66
- if (error instanceof HandlerNotFoundException) {
67
- console.log(`No handler for: ${error.requestName}`);
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
- ### No Migration Required
100
+ ### How It Works
73
101
 
74
- If you're using `NestMediatorModule.forRoot()`, no changes are needed. Pipeline behaviors are **opt-in** via `forRoot()`:
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
- ```typescript
77
- // Existing code - works exactly as before (no behaviors)
78
- NestMediatorModule.forRoot()
106
+ ### No Migration Required
79
107
 
80
- // New - opt-in to behaviors
81
- NestMediatorModule.forRoot({
82
- enableLogging: true,
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 queries.
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**: Error if no handler is registered for the command
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**: Error if no handler is registered for the query
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: