@shadow-library/fastify 0.0.7 → 0.0.9
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
CHANGED
|
@@ -444,6 +444,257 @@ export class ProductController {
|
|
|
444
444
|
}
|
|
445
445
|
```
|
|
446
446
|
|
|
447
|
+
## Context Service
|
|
448
|
+
|
|
449
|
+
The `ContextService` provides request-scoped context management using Node.js AsyncLocalStorage. It allows you to access request-specific data from anywhere in your application without explicitly passing it through function parameters.
|
|
450
|
+
|
|
451
|
+
**Important**: Context is automatically initialized for all HTTP requests and is always available within the request-response lifecycle (controllers, middleware, guards, services called during request processing). The `isInitialized()` method is primarily useful for methods that might be called both within and outside the request-response scope, such as during application startup, migrations, or background tasks.
|
|
452
|
+
|
|
453
|
+
### Accessing Context Service
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { ContextService } from '@shadow-library/fastify';
|
|
457
|
+
|
|
458
|
+
@HttpController('/api')
|
|
459
|
+
export class ExampleController {
|
|
460
|
+
constructor(private readonly contextService: ContextService) {}
|
|
461
|
+
|
|
462
|
+
@Get('/current-request')
|
|
463
|
+
getCurrentRequestInfo() {
|
|
464
|
+
const request = this.contextService.getRequest();
|
|
465
|
+
const rid = this.contextService.getRID();
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
method: request.method,
|
|
469
|
+
url: request.url,
|
|
470
|
+
requestId: rid,
|
|
471
|
+
userAgent: request.headers['user-agent'],
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Core Methods
|
|
478
|
+
|
|
479
|
+
#### Context State Management
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// Check if context is initialized
|
|
483
|
+
// Useful for methods that may be called outside request-response scope
|
|
484
|
+
// (e.g., during migrations, startup tasks, background jobs)
|
|
485
|
+
contextService.isInitialized(): boolean
|
|
486
|
+
|
|
487
|
+
// Check if running in a child context (for nested operations)
|
|
488
|
+
contextService.isChildContext(): boolean
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### Request/Response Access
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// Get the current HTTP request object
|
|
495
|
+
contextService.getRequest(): FastifyRequest
|
|
496
|
+
contextService.getRequest(false): FastifyRequest | null
|
|
497
|
+
|
|
498
|
+
// Get the current HTTP response object
|
|
499
|
+
contextService.getResponse(): FastifyReply
|
|
500
|
+
contextService.getResponse(false): FastifyReply | null
|
|
501
|
+
|
|
502
|
+
// Get the current request ID
|
|
503
|
+
contextService.getRID(): string
|
|
504
|
+
contextService.getRID(false): string | null
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### Data Storage
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// Store data in current context
|
|
511
|
+
contextService.set('user', userData);
|
|
512
|
+
contextService.set('startTime', Date.now());
|
|
513
|
+
|
|
514
|
+
// Retrieve data from current context
|
|
515
|
+
const user = contextService.get('user');
|
|
516
|
+
const startTime = contextService.get('startTime', true); // throws if missing
|
|
517
|
+
|
|
518
|
+
// Store data in parent context (when in child context)
|
|
519
|
+
contextService.setInParent('sharedData', value);
|
|
520
|
+
|
|
521
|
+
// Get data from parent context
|
|
522
|
+
const parentData = contextService.getFromParent('sharedData');
|
|
523
|
+
|
|
524
|
+
// Resolve data (checks current context first, then parent)
|
|
525
|
+
const resolvedData = contextService.resolve('someKey');
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Practical Examples
|
|
529
|
+
|
|
530
|
+
#### Service Used in Multiple Contexts
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
@Injectable()
|
|
534
|
+
export class UserService {
|
|
535
|
+
constructor(private readonly contextService: ContextService) {}
|
|
536
|
+
|
|
537
|
+
async getUserInfo(userId: string) {
|
|
538
|
+
// This service method might be called during HTTP requests
|
|
539
|
+
// OR during migrations/background tasks
|
|
540
|
+
if (this.contextService.isInitialized()) {
|
|
541
|
+
// We're in a request context - can access request-specific data
|
|
542
|
+
const requestId = this.contextService.getRID();
|
|
543
|
+
console.log(`Fetching user ${userId} for request ${requestId}`);
|
|
544
|
+
|
|
545
|
+
// Maybe add audit trail with request context
|
|
546
|
+
const request = this.contextService.getRequest();
|
|
547
|
+
await this.auditLog.log({
|
|
548
|
+
action: 'getUserInfo',
|
|
549
|
+
userId,
|
|
550
|
+
requestId,
|
|
551
|
+
userAgent: request.headers['user-agent'],
|
|
552
|
+
ip: request.ip,
|
|
553
|
+
});
|
|
554
|
+
} else {
|
|
555
|
+
// We're outside request context (migration, background task, etc.)
|
|
556
|
+
console.log(`Fetching user ${userId} outside request context`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return this.database.findUser(userId);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### Migration Script Example
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// During migrations, context is not initialized
|
|
568
|
+
class UserMigration {
|
|
569
|
+
constructor(private readonly userService: UserService) {}
|
|
570
|
+
|
|
571
|
+
async migrateBulkUsers() {
|
|
572
|
+
// Context is NOT initialized here
|
|
573
|
+
console.log('Context initialized:', this.contextService.isInitialized()); // false
|
|
574
|
+
|
|
575
|
+
const users = await this.userService.getAllUsers(); // Works fine
|
|
576
|
+
// Process users...
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### Request Logging Middleware
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
@Middleware({ type: 'onRequest', weight: 100 })
|
|
585
|
+
export class RequestLoggerMiddleware {
|
|
586
|
+
constructor(private readonly contextService: ContextService) {}
|
|
587
|
+
|
|
588
|
+
use(request: FastifyRequest, reply: FastifyReply, done: Function) {
|
|
589
|
+
// Context is ALWAYS initialized in middleware during requests
|
|
590
|
+
// No need to check isInitialized() here
|
|
591
|
+
this.contextService.set('startTime', Date.now());
|
|
592
|
+
this.contextService.set('userIP', request.ip);
|
|
593
|
+
done();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
@Middleware({ type: 'onResponse', weight: 100 })
|
|
598
|
+
export class ResponseLoggerMiddleware {
|
|
599
|
+
constructor(private readonly contextService: ContextService) {}
|
|
600
|
+
|
|
601
|
+
use(request: FastifyRequest, reply: FastifyReply, done: Function) {
|
|
602
|
+
// Context is ALWAYS initialized in middleware during requests
|
|
603
|
+
const startTime = this.contextService.get<number>('startTime');
|
|
604
|
+
const duration = startTime ? Date.now() - startTime : 0;
|
|
605
|
+
|
|
606
|
+
console.log({
|
|
607
|
+
requestId: this.contextService.getRID(),
|
|
608
|
+
method: request.method,
|
|
609
|
+
url: request.url,
|
|
610
|
+
statusCode: reply.statusCode,
|
|
611
|
+
duration: `${duration}ms`,
|
|
612
|
+
userIP: this.contextService.get('userIP')
|
|
613
|
+
});
|
|
614
|
+
done();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
console.log({
|
|
619
|
+
requestId: this.contextService.getRID(),
|
|
620
|
+
method: request.method,
|
|
621
|
+
url: request.url,
|
|
622
|
+
statusCode: reply.statusCode,
|
|
623
|
+
duration: `${duration}ms`,
|
|
624
|
+
userIP: this.contextService.get('userIP'),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
done();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Authentication Context
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
@Middleware({ type: 'preHandler', weight: 90 })
|
|
636
|
+
export class AuthMiddleware {
|
|
637
|
+
constructor(private readonly contextService: ContextService) {}
|
|
638
|
+
|
|
639
|
+
async use(request: FastifyRequest, reply: FastifyReply) {
|
|
640
|
+
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
641
|
+
|
|
642
|
+
if (token) {
|
|
643
|
+
// Context is always available in middleware during requests
|
|
644
|
+
const user = await this.validateToken(token);
|
|
645
|
+
if (user) {
|
|
646
|
+
// Store authenticated user in context
|
|
647
|
+
this.contextService.set('currentUser', user);
|
|
648
|
+
this.contextService.set('isAuthenticated', true);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Use in any controller or service
|
|
655
|
+
@HttpController('/api/profile')
|
|
656
|
+
export class ProfileController {
|
|
657
|
+
constructor(private readonly contextService: ContextService) {}
|
|
658
|
+
|
|
659
|
+
@Get()
|
|
660
|
+
getProfile() {
|
|
661
|
+
// Context is always available in controllers during requests
|
|
662
|
+
const isAuthenticated = this.contextService.get<boolean>('isAuthenticated');
|
|
663
|
+
if (!isAuthenticated) {
|
|
664
|
+
throw new ServerError(ServerErrorCode.UNAUTHORIZED);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const currentUser = this.contextService.get('currentUser');
|
|
668
|
+
return { user: currentUser };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### Child Context Usage
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
@HttpController('/api')
|
|
677
|
+
export class DataController {
|
|
678
|
+
constructor(
|
|
679
|
+
private readonly contextService: ContextService,
|
|
680
|
+
@Inject(Router) private readonly fastifyRouter: FastifyRouter,
|
|
681
|
+
) {}
|
|
682
|
+
|
|
683
|
+
@Get('/aggregate')
|
|
684
|
+
async getAggregateData() {
|
|
685
|
+
const results = [];
|
|
686
|
+
|
|
687
|
+
// Each child route call creates a new child context
|
|
688
|
+
for (const endpoint of ['/users', '/posts', '/comments']) {
|
|
689
|
+
const result = await this.fastifyRouter.resolveChildRoute(endpoint);
|
|
690
|
+
results.push(result);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return { aggregated: results };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
447
698
|
## Examples
|
|
448
699
|
|
|
449
700
|
Check out the [examples](./examples) directory for complete working examples:
|
|
@@ -5,6 +5,7 @@ export declare class ContextService {
|
|
|
5
5
|
static readonly name = "ContextService";
|
|
6
6
|
private readonly storage;
|
|
7
7
|
init(): onRequestHookHandler;
|
|
8
|
+
isInitialized(): boolean;
|
|
8
9
|
get<T>(key: Key, throwOnMissing: true): T;
|
|
9
10
|
get<T>(key: Key, throwOnMissing?: boolean): T | null;
|
|
10
11
|
getFromParent<T>(key: Key, throwOnMissing: true): T;
|
|
@@ -15,7 +16,10 @@ export declare class ContextService {
|
|
|
15
16
|
setInParent<T>(key: Key, value: T): this;
|
|
16
17
|
isChildContext(): boolean;
|
|
17
18
|
getRequest(): HttpRequest;
|
|
19
|
+
getRequest(throwOnMissing: false): HttpRequest | null;
|
|
18
20
|
getResponse(): HttpResponse;
|
|
21
|
+
getResponse(throwOnMissing: false): HttpResponse | null;
|
|
19
22
|
getRID(): string;
|
|
23
|
+
getRID(throwOnMissing: false): string | null;
|
|
20
24
|
}
|
|
21
25
|
export {};
|
|
@@ -37,6 +37,9 @@ let ContextService = class ContextService {
|
|
|
37
37
|
this.storage.run(store, done);
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
isInitialized() {
|
|
41
|
+
return this.storage.getStore() !== undefined;
|
|
42
|
+
}
|
|
40
43
|
get(key, throwOnMissing) {
|
|
41
44
|
const store = this.storage.getStore();
|
|
42
45
|
if (!store)
|
|
@@ -80,14 +83,14 @@ let ContextService = class ContextService {
|
|
|
80
83
|
const parentStore = this.get(PARENT_CONTEXT);
|
|
81
84
|
return parentStore !== null;
|
|
82
85
|
}
|
|
83
|
-
getRequest() {
|
|
84
|
-
return this.get(REQUEST,
|
|
86
|
+
getRequest(throwOnMissing = true) {
|
|
87
|
+
return this.get(REQUEST, throwOnMissing);
|
|
85
88
|
}
|
|
86
|
-
getResponse() {
|
|
87
|
-
return this.get(RESPONSE,
|
|
89
|
+
getResponse(throwOnMissing = true) {
|
|
90
|
+
return this.get(RESPONSE, throwOnMissing);
|
|
88
91
|
}
|
|
89
|
-
getRID() {
|
|
90
|
-
return this.get(RID,
|
|
92
|
+
getRID(throwOnMissing = true) {
|
|
93
|
+
return this.get(RID, throwOnMissing);
|
|
91
94
|
}
|
|
92
95
|
};
|
|
93
96
|
exports.ContextService = ContextService;
|
|
@@ -5,6 +5,7 @@ export declare class ContextService {
|
|
|
5
5
|
static readonly name = "ContextService";
|
|
6
6
|
private readonly storage;
|
|
7
7
|
init(): onRequestHookHandler;
|
|
8
|
+
isInitialized(): boolean;
|
|
8
9
|
get<T>(key: Key, throwOnMissing: true): T;
|
|
9
10
|
get<T>(key: Key, throwOnMissing?: boolean): T | null;
|
|
10
11
|
getFromParent<T>(key: Key, throwOnMissing: true): T;
|
|
@@ -15,7 +16,10 @@ export declare class ContextService {
|
|
|
15
16
|
setInParent<T>(key: Key, value: T): this;
|
|
16
17
|
isChildContext(): boolean;
|
|
17
18
|
getRequest(): HttpRequest;
|
|
19
|
+
getRequest(throwOnMissing: false): HttpRequest | null;
|
|
18
20
|
getResponse(): HttpResponse;
|
|
21
|
+
getResponse(throwOnMissing: false): HttpResponse | null;
|
|
19
22
|
getRID(): string;
|
|
23
|
+
getRID(throwOnMissing: false): string | null;
|
|
20
24
|
}
|
|
21
25
|
export {};
|
|
@@ -34,6 +34,9 @@ let ContextService = class ContextService {
|
|
|
34
34
|
this.storage.run(store, done);
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
isInitialized() {
|
|
38
|
+
return this.storage.getStore() !== undefined;
|
|
39
|
+
}
|
|
37
40
|
get(key, throwOnMissing) {
|
|
38
41
|
const store = this.storage.getStore();
|
|
39
42
|
if (!store)
|
|
@@ -77,14 +80,14 @@ let ContextService = class ContextService {
|
|
|
77
80
|
const parentStore = this.get(PARENT_CONTEXT);
|
|
78
81
|
return parentStore !== null;
|
|
79
82
|
}
|
|
80
|
-
getRequest() {
|
|
81
|
-
return this.get(REQUEST,
|
|
83
|
+
getRequest(throwOnMissing = true) {
|
|
84
|
+
return this.get(REQUEST, throwOnMissing);
|
|
82
85
|
}
|
|
83
|
-
getResponse() {
|
|
84
|
-
return this.get(RESPONSE,
|
|
86
|
+
getResponse(throwOnMissing = true) {
|
|
87
|
+
return this.get(RESPONSE, throwOnMissing);
|
|
85
88
|
}
|
|
86
|
-
getRID() {
|
|
87
|
-
return this.get(RID,
|
|
89
|
+
getRID(throwOnMissing = true) {
|
|
90
|
+
return this.get(RID, throwOnMissing);
|
|
88
91
|
}
|
|
89
92
|
};
|
|
90
93
|
ContextService = __decorate([
|
package/package.json
CHANGED