@rolandsall24/nest-mediator 0.4.3 → 0.5.1
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 +492 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/behaviors/exception-handling.behavior.d.ts +46 -0
- package/dist/lib/behaviors/exception-handling.behavior.d.ts.map +1 -0
- package/dist/lib/behaviors/exception-handling.behavior.js +76 -0
- package/dist/lib/behaviors/exception-handling.behavior.js.map +1 -0
- package/dist/lib/behaviors/index.d.ts +5 -0
- package/dist/lib/behaviors/index.d.ts.map +1 -0
- package/dist/lib/behaviors/index.js +21 -0
- package/dist/lib/behaviors/index.js.map +1 -0
- package/dist/lib/behaviors/logging.behavior.d.ts +26 -0
- package/dist/lib/behaviors/logging.behavior.d.ts.map +1 -0
- package/dist/lib/behaviors/logging.behavior.js +64 -0
- package/dist/lib/behaviors/logging.behavior.js.map +1 -0
- package/dist/lib/behaviors/performance.behavior.d.ts +41 -0
- package/dist/lib/behaviors/performance.behavior.d.ts.map +1 -0
- package/dist/lib/behaviors/performance.behavior.js +71 -0
- package/dist/lib/behaviors/performance.behavior.js.map +1 -0
- package/dist/lib/behaviors/validation.behavior.d.ts +69 -0
- package/dist/lib/behaviors/validation.behavior.d.ts.map +1 -0
- package/dist/lib/behaviors/validation.behavior.js +137 -0
- package/dist/lib/behaviors/validation.behavior.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 +51 -0
- package/dist/lib/decorators/pipeline-behavior.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/pipeline-behavior.decorator.js +68 -0
- package/dist/lib/decorators/pipeline-behavior.decorator.js.map +1 -0
- package/dist/lib/decorators/skip-behavior.decorator.d.ts +52 -0
- package/dist/lib/decorators/skip-behavior.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/skip-behavior.decorator.js +61 -0
- package/dist/lib/decorators/skip-behavior.decorator.js.map +1 -0
- package/dist/lib/exceptions/handler-not-found.exception.d.ts +20 -0
- package/dist/lib/exceptions/handler-not-found.exception.d.ts.map +1 -0
- package/dist/lib/exceptions/handler-not-found.exception.js +33 -0
- package/dist/lib/exceptions/handler-not-found.exception.js.map +1 -0
- package/dist/lib/exceptions/index.d.ts +3 -0
- package/dist/lib/exceptions/index.d.ts.map +1 -0
- package/dist/lib/exceptions/index.js +19 -0
- package/dist/lib/exceptions/index.js.map +1 -0
- package/dist/lib/exceptions/validation.exception.d.ts +62 -0
- package/dist/lib/exceptions/validation.exception.d.ts.map +1 -0
- package/dist/lib/exceptions/validation.exception.js +66 -0
- package/dist/lib/exceptions/validation.exception.js.map +1 -0
- package/dist/lib/interfaces/index.d.ts +1 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -1
- package/dist/lib/interfaces/index.js +1 -0
- package/dist/lib/interfaces/index.js.map +1 -1
- package/dist/lib/interfaces/pipeline-behavior.interface.d.ts +60 -0
- package/dist/lib/interfaces/pipeline-behavior.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/pipeline-behavior.interface.js +3 -0
- package/dist/lib/interfaces/pipeline-behavior.interface.js.map +1 -0
- package/dist/lib/nest-mediator.module.d.ts +73 -4
- package/dist/lib/nest-mediator.module.d.ts.map +1 -1
- package/dist/lib/nest-mediator.module.js +63 -7
- package/dist/lib/nest-mediator.module.js.map +1 -1
- package/dist/lib/services/mediator.bus.d.ts +54 -4
- package/dist/lib/services/mediator.bus.d.ts.map +1 -1
- package/dist/lib/services/mediator.bus.js +104 -9
- package/dist/lib/services/mediator.bus.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ A lightweight CQRS (Command Query Responsibility Segregation) mediator pattern i
|
|
|
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.)
|
|
12
|
+
- Built-in behaviors: Logging, Validation, Exception Handling, Performance Tracking
|
|
11
13
|
- Built on top of NestJS dependency injection
|
|
12
14
|
- Zero runtime dependencies beyond NestJS
|
|
13
15
|
|
|
@@ -30,6 +32,58 @@ This library requires TypeScript decorators to be enabled. Add the following to
|
|
|
30
32
|
}
|
|
31
33
|
```
|
|
32
34
|
|
|
35
|
+
## Upgrading to v0.5.0
|
|
36
|
+
|
|
37
|
+
Version 0.5.0 introduces **Pipeline Behaviors** while maintaining backward compatibility. Existing code using `NestMediatorModule.forRoot()` will continue to work without changes.
|
|
38
|
+
|
|
39
|
+
### What's New
|
|
40
|
+
|
|
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
|
|
45
|
+
|
|
46
|
+
### Breaking Change Notice
|
|
47
|
+
|
|
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:
|
|
49
|
+
|
|
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
|
+
}
|
|
59
|
+
|
|
60
|
+
// After (optional - for more specific handling)
|
|
61
|
+
import { HandlerNotFoundException } from '@rolandsall24/nest-mediator';
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await mediator.send(command);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof HandlerNotFoundException) {
|
|
67
|
+
console.log(`No handler for: ${error.requestName}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### No Migration Required
|
|
73
|
+
|
|
74
|
+
If you're using `NestMediatorModule.forRoot()`, no changes are needed. Pipeline behaviors are **opt-in** via `forRoot()`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Existing code - works exactly as before (no behaviors)
|
|
78
|
+
NestMediatorModule.forRoot()
|
|
79
|
+
|
|
80
|
+
// New - opt-in to behaviors
|
|
81
|
+
NestMediatorModule.forRoot({
|
|
82
|
+
enableLogging: true,
|
|
83
|
+
enableValidation: true,
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
33
87
|
## Quick Start
|
|
34
88
|
|
|
35
89
|
### 1. Import the Module
|
|
@@ -44,6 +98,7 @@ import { GetUserQueryHandler } from './handlers/get-user-query.handler';
|
|
|
44
98
|
|
|
45
99
|
@Module({
|
|
46
100
|
imports: [
|
|
101
|
+
// Basic setup
|
|
47
102
|
NestMediatorModule.forRoot(),
|
|
48
103
|
],
|
|
49
104
|
providers: [
|
|
@@ -56,7 +111,26 @@ import { GetUserQueryHandler } from './handlers/get-user-query.handler';
|
|
|
56
111
|
export class AppModule {}
|
|
57
112
|
```
|
|
58
113
|
|
|
59
|
-
|
|
114
|
+
Or with built-in pipeline behaviors enabled:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
@Module({
|
|
118
|
+
imports: [
|
|
119
|
+
NestMediatorModule.forRoot({
|
|
120
|
+
enableLogging: true, // Log request handling with timing
|
|
121
|
+
enableValidation: true, // Validate requests with class-validator
|
|
122
|
+
enableExceptionHandling: true, // Centralized exception logging
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
providers: [
|
|
126
|
+
CreateUserCommandHandler,
|
|
127
|
+
GetUserQueryHandler,
|
|
128
|
+
],
|
|
129
|
+
})
|
|
130
|
+
export class AppModule {}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**How it works**: The module uses NestJS's `DiscoveryService` to automatically discover and register all providers decorated with `@CommandHandler`, `@QueryHandler`, or `@PipelineBehavior`. Simply add your handlers to the module's `providers` array and they will be automatically registered with the mediator!
|
|
60
134
|
|
|
61
135
|
## Usage
|
|
62
136
|
|
|
@@ -510,7 +584,12 @@ import { UserPersistenceAdapter } from './infrastructure/persistence/user/user-p
|
|
|
510
584
|
|
|
511
585
|
@Module({
|
|
512
586
|
imports: [
|
|
513
|
-
|
|
587
|
+
// Enable pipeline behaviors for logging, validation, and error handling
|
|
588
|
+
NestMediatorModule.forRoot({
|
|
589
|
+
enableLogging: true,
|
|
590
|
+
enableValidation: true,
|
|
591
|
+
enableExceptionHandling: true,
|
|
592
|
+
}),
|
|
514
593
|
],
|
|
515
594
|
controllers: [UserController],
|
|
516
595
|
providers: [
|
|
@@ -593,6 +672,16 @@ export interface IQueryHandler<TQuery extends IQuery, TResult = any> {
|
|
|
593
672
|
}
|
|
594
673
|
```
|
|
595
674
|
|
|
675
|
+
#### `IPipelineBehavior<TRequest, TResponse>`
|
|
676
|
+
|
|
677
|
+
Interface for pipeline behaviors (cross-cutting concerns).
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
export interface IPipelineBehavior<TRequest = any, TResponse = any> {
|
|
681
|
+
handle(request: TRequest, next: () => Promise<TResponse>): Promise<TResponse>;
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
596
685
|
### Decorators
|
|
597
686
|
|
|
598
687
|
#### `@CommandHandler(command)`
|
|
@@ -609,6 +698,25 @@ Marks a class as a query handler.
|
|
|
609
698
|
- **Parameters**: `query` - The query class this handler handles
|
|
610
699
|
- **Usage**: Apply to handler classes that implement `IQueryHandler`
|
|
611
700
|
|
|
701
|
+
#### `@PipelineBehavior(options?)`
|
|
702
|
+
|
|
703
|
+
Marks a class as a pipeline behavior.
|
|
704
|
+
|
|
705
|
+
- **Parameters**:
|
|
706
|
+
- `options.priority` - Execution order (lower numbers execute first, default: 0)
|
|
707
|
+
- `options.scope` - `'command'`, `'query'`, or `'all'` (default: `'all'`)
|
|
708
|
+
- **Usage**: Apply to behavior classes that implement `IPipelineBehavior`
|
|
709
|
+
|
|
710
|
+
#### `@SkipBehavior(behavior | behaviors[])`
|
|
711
|
+
|
|
712
|
+
Excludes specific pipeline behaviors from a command or query.
|
|
713
|
+
|
|
714
|
+
- **Parameters**:
|
|
715
|
+
- Single behavior class: `@SkipBehavior(PerformanceBehavior)`
|
|
716
|
+
- Array of behavior classes: `@SkipBehavior([PerformanceBehavior, LoggingBehavior])`
|
|
717
|
+
- **Usage**: Apply to command or query classes
|
|
718
|
+
- **Works with**: Both built-in behaviors and custom behaviors
|
|
719
|
+
|
|
612
720
|
### Services
|
|
613
721
|
|
|
614
722
|
#### `MediatorBus`
|
|
@@ -633,6 +741,388 @@ Executes a query through its registered handler.
|
|
|
633
741
|
- **Returns**: Promise that resolves with the query result
|
|
634
742
|
- **Throws**: Error if no handler is registered for the query
|
|
635
743
|
|
|
744
|
+
### Module Configuration
|
|
745
|
+
|
|
746
|
+
#### `NestMediatorModule.forRoot()`
|
|
747
|
+
|
|
748
|
+
Basic module registration with no built-in behaviors.
|
|
749
|
+
|
|
750
|
+
#### `NestMediatorModule.forRoot(options)`
|
|
751
|
+
|
|
752
|
+
Module registration with configuration options.
|
|
753
|
+
|
|
754
|
+
| Option | Type | Default | Description |
|
|
755
|
+
|--------|------|---------|-------------|
|
|
756
|
+
| `enableLogging` | boolean | false | Enable request/response logging |
|
|
757
|
+
| `enableValidation` | boolean | false | Enable class-validator validation |
|
|
758
|
+
| `enableExceptionHandling` | boolean | false | Enable centralized exception logging |
|
|
759
|
+
| `enablePerformanceTracking` | boolean | false | Enable slow request warnings |
|
|
760
|
+
| `performanceThresholdMs` | number | 500 | Threshold for slow request warnings |
|
|
761
|
+
| `behaviors` | Type[] | [] | Additional custom behaviors to register |
|
|
762
|
+
|
|
763
|
+
## Pipeline Behaviors
|
|
764
|
+
|
|
765
|
+
Pipeline behaviors allow you to add cross-cutting concerns (like logging, validation, caching) that execute around every command and query handler.
|
|
766
|
+
|
|
767
|
+
### Enabling Built-in Behaviors
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
import { Module } from '@nestjs/common';
|
|
771
|
+
import { NestMediatorModule } from '@rolandsall24/nest-mediator';
|
|
772
|
+
|
|
773
|
+
@Module({
|
|
774
|
+
imports: [
|
|
775
|
+
NestMediatorModule.forRoot({
|
|
776
|
+
enableLogging: true, // Logs request handling with timing
|
|
777
|
+
enableValidation: true, // Validates requests using class-validator
|
|
778
|
+
enableExceptionHandling: true, // Centralized exception logging
|
|
779
|
+
enablePerformanceTracking: true, // Warns on slow requests
|
|
780
|
+
performanceThresholdMs: 500, // Threshold for slow request warnings
|
|
781
|
+
}),
|
|
782
|
+
],
|
|
783
|
+
})
|
|
784
|
+
export class AppModule {}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Built-in Behaviors
|
|
788
|
+
|
|
789
|
+
| Behavior | Priority | Description |
|
|
790
|
+
|----------|----------|-------------|
|
|
791
|
+
| `ExceptionHandlingBehavior` | -100 | Catches and logs all exceptions |
|
|
792
|
+
| `LoggingBehavior` | 0 | Logs request handling with timing |
|
|
793
|
+
| `PerformanceBehavior` | 10 | Warns when requests exceed threshold |
|
|
794
|
+
| `ValidationBehavior` | 100 | Validates requests using class-validator |
|
|
795
|
+
|
|
796
|
+
### Creating Custom Behaviors
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import { Injectable } from '@nestjs/common';
|
|
800
|
+
import { IPipelineBehavior, PipelineBehavior } from '@rolandsall24/nest-mediator';
|
|
801
|
+
|
|
802
|
+
@Injectable()
|
|
803
|
+
@PipelineBehavior({ priority: 50, scope: 'all' })
|
|
804
|
+
export class MyCustomBehavior<TRequest, TResponse>
|
|
805
|
+
implements IPipelineBehavior<TRequest, TResponse> {
|
|
806
|
+
|
|
807
|
+
async handle(
|
|
808
|
+
request: TRequest,
|
|
809
|
+
next: () => Promise<TResponse>
|
|
810
|
+
): Promise<TResponse> {
|
|
811
|
+
// Pre-processing logic
|
|
812
|
+
console.log('Before handler:', request);
|
|
813
|
+
|
|
814
|
+
// Call the next behavior or handler
|
|
815
|
+
const response = await next();
|
|
816
|
+
|
|
817
|
+
// Post-processing logic
|
|
818
|
+
console.log('After handler:', response);
|
|
819
|
+
|
|
820
|
+
return response;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Behavior Options
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
@PipelineBehavior({
|
|
829
|
+
priority: 50, // Lower numbers execute first (outermost)
|
|
830
|
+
scope: 'command', // 'command', 'query', or 'all'
|
|
831
|
+
})
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**Priority Guidelines:**
|
|
835
|
+
- `-100 to -1`: Exception handling (outermost)
|
|
836
|
+
- `0 to 99`: Logging, performance tracking
|
|
837
|
+
- `100 to 199`: Validation
|
|
838
|
+
- `200+`: Transaction/Unit of Work (innermost)
|
|
839
|
+
|
|
840
|
+
### Registering Custom Behaviors
|
|
841
|
+
|
|
842
|
+
Add your behavior to the module providers:
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
@Module({
|
|
846
|
+
imports: [
|
|
847
|
+
NestMediatorModule.forRoot({
|
|
848
|
+
behaviors: [MyCustomBehavior],
|
|
849
|
+
}),
|
|
850
|
+
],
|
|
851
|
+
providers: [
|
|
852
|
+
MyCustomBehavior, // Also add to providers for DI
|
|
853
|
+
],
|
|
854
|
+
})
|
|
855
|
+
export class AppModule {}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Or use the `@PipelineBehavior` decorator and add to providers - it will be auto-discovered:
|
|
859
|
+
|
|
860
|
+
```typescript
|
|
861
|
+
@Module({
|
|
862
|
+
imports: [NestMediatorModule.forRoot()],
|
|
863
|
+
providers: [MyCustomBehavior], // Auto-discovered via decorator
|
|
864
|
+
})
|
|
865
|
+
export class AppModule {}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Skipping Behaviors for Specific Commands/Queries
|
|
869
|
+
|
|
870
|
+
Use `@SkipBehavior` to exclude specific behaviors from a command or query:
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
import {
|
|
874
|
+
ICommand,
|
|
875
|
+
SkipBehavior,
|
|
876
|
+
PerformanceBehavior,
|
|
877
|
+
LoggingBehavior,
|
|
878
|
+
} from '@rolandsall24/nest-mediator';
|
|
879
|
+
|
|
880
|
+
// Skip a single behavior
|
|
881
|
+
@SkipBehavior(PerformanceBehavior)
|
|
882
|
+
export class HighFrequencyCommand implements ICommand {
|
|
883
|
+
// This command will not trigger performance tracking
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Skip multiple behaviors
|
|
887
|
+
@SkipBehavior([PerformanceBehavior, LoggingBehavior])
|
|
888
|
+
export class HealthCheckQuery implements IQuery {
|
|
889
|
+
// This query skips both performance and logging behaviors
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
This works with both built-in behaviors and custom behaviors:
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
import { SkipBehavior } from '@rolandsall24/nest-mediator';
|
|
897
|
+
import { MyAuditBehavior } from './behaviors/audit.behavior';
|
|
898
|
+
|
|
899
|
+
@SkipBehavior(MyAuditBehavior)
|
|
900
|
+
export class InternalCommand implements ICommand {
|
|
901
|
+
// Skips your custom audit behavior
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### Custom Behavior with Service Injection
|
|
906
|
+
|
|
907
|
+
Behaviors support full NestJS dependency injection:
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
911
|
+
import { IPipelineBehavior, PipelineBehavior } from '@rolandsall24/nest-mediator';
|
|
912
|
+
|
|
913
|
+
// Service for audit logging
|
|
914
|
+
@Injectable()
|
|
915
|
+
export class AuditService {
|
|
916
|
+
private readonly logger = new Logger(AuditService.name);
|
|
917
|
+
|
|
918
|
+
async logAction(action: string, userId: string): Promise<void> {
|
|
919
|
+
this.logger.log(`[AUDIT] ${action} by ${userId}`);
|
|
920
|
+
// Save to database, send to external service, etc.
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Behavior that uses the service
|
|
925
|
+
@Injectable()
|
|
926
|
+
@PipelineBehavior({ priority: 50, scope: 'command' })
|
|
927
|
+
export class AuditLoggingBehavior<TRequest, TResponse>
|
|
928
|
+
implements IPipelineBehavior<TRequest, TResponse> {
|
|
929
|
+
|
|
930
|
+
constructor(private readonly auditService: AuditService) {}
|
|
931
|
+
|
|
932
|
+
async handle(
|
|
933
|
+
request: TRequest,
|
|
934
|
+
next: () => Promise<TResponse>,
|
|
935
|
+
): Promise<TResponse> {
|
|
936
|
+
const requestName = request.constructor.name;
|
|
937
|
+
|
|
938
|
+
await this.auditService.logAction(`Executing ${requestName}`, 'user-123');
|
|
939
|
+
|
|
940
|
+
const response = await next();
|
|
941
|
+
|
|
942
|
+
await this.auditService.logAction(`Completed ${requestName}`, 'user-123');
|
|
943
|
+
|
|
944
|
+
return response;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Register both in your module
|
|
949
|
+
@Module({
|
|
950
|
+
imports: [NestMediatorModule.forRoot()],
|
|
951
|
+
providers: [
|
|
952
|
+
AuditService, // The service
|
|
953
|
+
AuditLoggingBehavior, // The behavior (auto-discovered)
|
|
954
|
+
],
|
|
955
|
+
})
|
|
956
|
+
export class AppModule {}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Advanced Behavior Examples
|
|
960
|
+
|
|
961
|
+
#### Retry Behavior (for transient failures)
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
@Injectable()
|
|
965
|
+
@PipelineBehavior({ priority: -50, scope: 'command' })
|
|
966
|
+
export class RetryBehavior<TRequest, TResponse>
|
|
967
|
+
implements IPipelineBehavior<TRequest, TResponse> {
|
|
968
|
+
|
|
969
|
+
private readonly maxRetries = 3;
|
|
970
|
+
|
|
971
|
+
async handle(
|
|
972
|
+
request: TRequest,
|
|
973
|
+
next: () => Promise<TResponse>,
|
|
974
|
+
): Promise<TResponse> {
|
|
975
|
+
let lastError: Error;
|
|
976
|
+
|
|
977
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
978
|
+
try {
|
|
979
|
+
return await next();
|
|
980
|
+
} catch (error) {
|
|
981
|
+
lastError = error as Error;
|
|
982
|
+
|
|
983
|
+
// Don't retry validation errors
|
|
984
|
+
if (error.name?.includes('Validation')) throw error;
|
|
985
|
+
|
|
986
|
+
if (attempt < this.maxRetries) {
|
|
987
|
+
const delay = 100 * Math.pow(2, attempt - 1); // Exponential backoff
|
|
988
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
throw lastError!;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
#### Caching Behavior (for queries)
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
@Injectable()
|
|
1002
|
+
@PipelineBehavior({ priority: 5, scope: 'query' })
|
|
1003
|
+
export class CachingBehavior<TRequest, TResponse>
|
|
1004
|
+
implements IPipelineBehavior<TRequest, TResponse> {
|
|
1005
|
+
|
|
1006
|
+
private cache = new Map<string, { data: any; expiry: number }>();
|
|
1007
|
+
|
|
1008
|
+
async handle(
|
|
1009
|
+
request: TRequest,
|
|
1010
|
+
next: () => Promise<TResponse>,
|
|
1011
|
+
): Promise<TResponse> {
|
|
1012
|
+
const key = JSON.stringify(request);
|
|
1013
|
+
const cached = this.cache.get(key);
|
|
1014
|
+
|
|
1015
|
+
if (cached && cached.expiry > Date.now()) {
|
|
1016
|
+
return cached.data; // Cache hit
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const result = await next();
|
|
1020
|
+
|
|
1021
|
+
this.cache.set(key, {
|
|
1022
|
+
data: result,
|
|
1023
|
+
expiry: Date.now() + 30000, // 30 second TTL
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
#### Authorization Behavior
|
|
1032
|
+
|
|
1033
|
+
```typescript
|
|
1034
|
+
@Injectable()
|
|
1035
|
+
@PipelineBehavior({ priority: 25, scope: 'all' })
|
|
1036
|
+
export class AuthorizationBehavior<TRequest, TResponse>
|
|
1037
|
+
implements IPipelineBehavior<TRequest, TResponse> {
|
|
1038
|
+
|
|
1039
|
+
constructor(private readonly authService: AuthService) {}
|
|
1040
|
+
|
|
1041
|
+
private readonly adminOnlyCommands = ['DeleteUserCommand'];
|
|
1042
|
+
|
|
1043
|
+
async handle(
|
|
1044
|
+
request: TRequest,
|
|
1045
|
+
next: () => Promise<TResponse>,
|
|
1046
|
+
): Promise<TResponse> {
|
|
1047
|
+
const requestName = request.constructor.name;
|
|
1048
|
+
|
|
1049
|
+
if (!this.authService.isAuthenticated()) {
|
|
1050
|
+
throw new UnauthorizedException('Authentication required');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (this.adminOnlyCommands.includes(requestName)) {
|
|
1054
|
+
if (!this.authService.hasRole('admin')) {
|
|
1055
|
+
throw new ForbiddenException('Admin role required');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return next();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Complete Behavior Execution Order Example
|
|
1065
|
+
|
|
1066
|
+
With the following behaviors configured:
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
// Built-in behaviors (when enabled):
|
|
1070
|
+
// ExceptionHandlingBehavior: priority -100, scope 'all'
|
|
1071
|
+
// LoggingBehavior: priority 0, scope 'all'
|
|
1072
|
+
// PerformanceBehavior: priority 10, scope 'all'
|
|
1073
|
+
// ValidationBehavior: priority 100, scope 'all'
|
|
1074
|
+
|
|
1075
|
+
// Custom behaviors:
|
|
1076
|
+
// RetryBehavior: priority -50, scope 'command'
|
|
1077
|
+
// CachingBehavior: priority 5, scope 'query'
|
|
1078
|
+
// AuthorizationBehavior: priority 25, scope 'all'
|
|
1079
|
+
// AuditLoggingBehavior: priority 50, scope 'command'
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
**For a Command:**
|
|
1083
|
+
```
|
|
1084
|
+
Request → Exception(-100) → Retry(-50) → Logging(0) → Performance(10) → Authorization(25) → Audit(50) → Validation(100) → Handler
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
**For a Query:**
|
|
1088
|
+
```
|
|
1089
|
+
Request → Exception(-100) → Logging(0) → Caching(5) → Performance(10) → Authorization(25) → Validation(100) → Handler
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Validation with class-validator
|
|
1093
|
+
|
|
1094
|
+
When `enableValidation` is true, requests are validated using class-validator (if installed):
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
import { IsEmail, IsString, MinLength } from 'class-validator';
|
|
1098
|
+
import { ICommand } from '@rolandsall24/nest-mediator';
|
|
1099
|
+
|
|
1100
|
+
export class CreateUserCommand implements ICommand {
|
|
1101
|
+
@IsEmail()
|
|
1102
|
+
email: string;
|
|
1103
|
+
|
|
1104
|
+
@IsString()
|
|
1105
|
+
@MinLength(2)
|
|
1106
|
+
name: string;
|
|
1107
|
+
|
|
1108
|
+
constructor(email: string, name: string) {
|
|
1109
|
+
this.email = email;
|
|
1110
|
+
this.name = name;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// If validation fails, ValidationException is thrown with details
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### Pipeline Execution Order
|
|
1118
|
+
|
|
1119
|
+
With behaviors at priorities -100, 0, 10, 100:
|
|
1120
|
+
|
|
1121
|
+
```
|
|
1122
|
+
Request → ExceptionHandling(-100) → Logging(0) → Performance(10) → Validation(100) → Handler
|
|
1123
|
+
Response ← ExceptionHandling(-100) ← Logging(0) ← Performance(10) ← Validation(100) ← Handler
|
|
1124
|
+
```
|
|
1125
|
+
|
|
636
1126
|
## Best Practices
|
|
637
1127
|
|
|
638
1128
|
1. **Keep Commands and Queries Simple**: They should be simple data containers with minimal logic.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './lib/interfaces/index.js';
|
|
2
2
|
export * from './lib/decorators/index.js';
|
|
3
3
|
export * from './lib/services/index.js';
|
|
4
|
+
export * from './lib/behaviors/index.js';
|
|
5
|
+
export * from './lib/exceptions/index.js';
|
|
4
6
|
export * from './lib/nest-mediator.module.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,2BAA2B,CAAC;AAG1C,cAAc,2BAA2B,CAAC;AAG1C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,2BAA2B,CAAC;AAG1C,cAAc,2BAA2B,CAAC;AAG1C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,0BAA0B,CAAC;AAGzC,cAAc,2BAA2B,CAAC;AAG1C,cAAc,+BAA+B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,10 @@ __exportStar(require("./lib/interfaces/index.js"), exports);
|
|
|
20
20
|
__exportStar(require("./lib/decorators/index.js"), exports);
|
|
21
21
|
// Services
|
|
22
22
|
__exportStar(require("./lib/services/index.js"), exports);
|
|
23
|
+
// Behaviors
|
|
24
|
+
__exportStar(require("./lib/behaviors/index.js"), exports);
|
|
25
|
+
// Exceptions
|
|
26
|
+
__exportStar(require("./lib/exceptions/index.js"), exports);
|
|
23
27
|
// Module
|
|
24
28
|
__exportStar(require("./lib/nest-mediator.module.js"), exports);
|
|
25
29
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,aAAa;AACb,4DAA0C;AAE1C,aAAa;AACb,4DAA0C;AAE1C,WAAW;AACX,0DAAwC;AAExC,SAAS;AACT,gEAA8C"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,aAAa;AACb,4DAA0C;AAE1C,aAAa;AACb,4DAA0C;AAE1C,WAAW;AACX,0DAAwC;AAExC,YAAY;AACZ,2DAAyC;AAEzC,aAAa;AACb,4DAA0C;AAE1C,SAAS;AACT,gEAA8C"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IPipelineBehavior } from '../interfaces/pipeline-behavior.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interface for custom exception handlers.
|
|
4
|
+
* Implement this to transform or handle specific exception types.
|
|
5
|
+
*/
|
|
6
|
+
export interface IExceptionHandler {
|
|
7
|
+
/**
|
|
8
|
+
* Check if this handler can handle the given error
|
|
9
|
+
*/
|
|
10
|
+
canHandle(error: Error): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Handle the error - can transform it, log it, or rethrow
|
|
13
|
+
*/
|
|
14
|
+
handle(error: Error, request: any): Error | Promise<Error>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pipeline behavior that provides centralized exception handling.
|
|
18
|
+
* This behavior wraps all other behaviors and handlers, catching any exceptions.
|
|
19
|
+
*
|
|
20
|
+
* Priority: -100 (executes first/outermost to catch all exceptions)
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Catches all exceptions from the pipeline
|
|
24
|
+
* - Logs exceptions with request context
|
|
25
|
+
* - Can be extended with custom exception handlers
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // The behavior automatically catches and logs all exceptions
|
|
30
|
+
* await mediator.send(new CreateUserCommand('invalid'));
|
|
31
|
+
* // If handler throws, ExceptionHandlingBehavior logs:
|
|
32
|
+
* // [MediatorBus] Exception in CreateUserCommand: User validation failed
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class ExceptionHandlingBehavior<TRequest = any, TResponse = any> implements IPipelineBehavior<TRequest, TResponse> {
|
|
36
|
+
private readonly logger;
|
|
37
|
+
private readonly exceptionHandlers;
|
|
38
|
+
/**
|
|
39
|
+
* Register a custom exception handler
|
|
40
|
+
*/
|
|
41
|
+
registerExceptionHandler(handler: IExceptionHandler): void;
|
|
42
|
+
handle(request: TRequest, next: () => Promise<TResponse>): Promise<TResponse>;
|
|
43
|
+
private processException;
|
|
44
|
+
private getRequestName;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=exception-handling.behavior.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exception-handling.behavior.d.ts","sourceRoot":"","sources":["../../../src/lib/behaviors/exception-handling.behavior.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAGjF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IAEjC;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAEa,yBAAyB,CAAC,QAAQ,GAAG,GAAG,EAAE,SAAS,GAAG,GAAG,CACpE,YAAW,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC;IAEjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAE7D;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAIpD,MAAM,CACV,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAC7B,OAAO,CAAC,SAAS,CAAC;YAYP,gBAAgB;IAuB9B,OAAO,CAAC,cAAc;CAMvB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ExceptionHandlingBehavior = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const pipeline_behavior_decorator_js_1 = require("../decorators/pipeline-behavior.decorator.js");
|
|
12
|
+
/**
|
|
13
|
+
* Pipeline behavior that provides centralized exception handling.
|
|
14
|
+
* This behavior wraps all other behaviors and handlers, catching any exceptions.
|
|
15
|
+
*
|
|
16
|
+
* Priority: -100 (executes first/outermost to catch all exceptions)
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Catches all exceptions from the pipeline
|
|
20
|
+
* - Logs exceptions with request context
|
|
21
|
+
* - Can be extended with custom exception handlers
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // The behavior automatically catches and logs all exceptions
|
|
26
|
+
* await mediator.send(new CreateUserCommand('invalid'));
|
|
27
|
+
* // If handler throws, ExceptionHandlingBehavior logs:
|
|
28
|
+
* // [MediatorBus] Exception in CreateUserCommand: User validation failed
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
let ExceptionHandlingBehavior = class ExceptionHandlingBehavior {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.logger = new common_1.Logger('MediatorBus');
|
|
34
|
+
this.exceptionHandlers = [];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register a custom exception handler
|
|
38
|
+
*/
|
|
39
|
+
registerExceptionHandler(handler) {
|
|
40
|
+
this.exceptionHandlers.push(handler);
|
|
41
|
+
}
|
|
42
|
+
async handle(request, next) {
|
|
43
|
+
try {
|
|
44
|
+
return await next();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const processedError = await this.processException(error, request);
|
|
48
|
+
throw processedError;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async processException(error, request) {
|
|
52
|
+
const requestName = this.getRequestName(request);
|
|
53
|
+
// Log the exception
|
|
54
|
+
this.logger.error(`Exception in ${requestName}: ${error.message}`, error.stack);
|
|
55
|
+
// Try custom exception handlers
|
|
56
|
+
for (const handler of this.exceptionHandlers) {
|
|
57
|
+
if (handler.canHandle(error)) {
|
|
58
|
+
return handler.handle(error, request);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Return original error if no handler processed it
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
getRequestName(request) {
|
|
65
|
+
if (request && typeof request === 'object' && request.constructor) {
|
|
66
|
+
return request.constructor.name;
|
|
67
|
+
}
|
|
68
|
+
return 'UnknownRequest';
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
exports.ExceptionHandlingBehavior = ExceptionHandlingBehavior;
|
|
72
|
+
exports.ExceptionHandlingBehavior = ExceptionHandlingBehavior = __decorate([
|
|
73
|
+
(0, common_1.Injectable)(),
|
|
74
|
+
(0, pipeline_behavior_decorator_js_1.PipelineBehavior)({ priority: -100, scope: 'all' })
|
|
75
|
+
], ExceptionHandlingBehavior);
|
|
76
|
+
//# sourceMappingURL=exception-handling.behavior.js.map
|