@shadow-library/fastify 0.0.8 → 0.0.10
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,315 @@ 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
|
+
|
|
698
|
+
### Extending Context Service
|
|
699
|
+
|
|
700
|
+
The `ContextService` can be extended with custom methods to add application-specific functionality while maintaining type safety and method chaining capabilities.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import { contextService } from '@shadow-library/fastify';
|
|
704
|
+
|
|
705
|
+
declare module '@shadow-library/fastify' {
|
|
706
|
+
export interface ContextExtension {
|
|
707
|
+
setUserRole(role: string): ContextService;
|
|
708
|
+
getUserRole(): string;
|
|
709
|
+
setCurrentUserId(userId: string): ContextService;
|
|
710
|
+
getCurrentUserId(): string;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Extend the context service with custom methods
|
|
715
|
+
contextService.extend({
|
|
716
|
+
setUserRole(role: string) {
|
|
717
|
+
return this.set('user-role', role);
|
|
718
|
+
},
|
|
719
|
+
getUserRole() {
|
|
720
|
+
return this.get<string>('user-role', false);
|
|
721
|
+
},
|
|
722
|
+
setCurrentUserId(userId: string) {
|
|
723
|
+
return this.set('current-user-id', userId);
|
|
724
|
+
},
|
|
725
|
+
getCurrentUserId() {
|
|
726
|
+
return this.get<string>('current-user-id', false);
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Use in controllers with method chaining
|
|
731
|
+
@HttpController('/api')
|
|
732
|
+
export class UserController {
|
|
733
|
+
constructor(private readonly contextService: ContextService) {}
|
|
734
|
+
|
|
735
|
+
@Post('/login')
|
|
736
|
+
async login(@Body() loginDto: LoginDto) {
|
|
737
|
+
const user = await this.authService.validateUser(loginDto);
|
|
738
|
+
|
|
739
|
+
// Chain extended methods
|
|
740
|
+
this.contextService.setCurrentUserId(user.id).setUserRole(user.role);
|
|
741
|
+
|
|
742
|
+
return { message: 'Login successful' };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
@Get('/profile')
|
|
746
|
+
getProfile() {
|
|
747
|
+
return {
|
|
748
|
+
userId: this.contextService.getCurrentUserId(),
|
|
749
|
+
role: this.contextService.getUserRole(),
|
|
750
|
+
requestId: this.contextService.getRID(),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
447
756
|
## Examples
|
|
448
757
|
|
|
449
758
|
Check out the [examples](./examples) directory for complete working examples:
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { onRequestHookHandler } from 'fastify';
|
|
2
2
|
import { HttpRequest, HttpResponse } from '../interfaces/index.js';
|
|
3
3
|
type Key = string | symbol;
|
|
4
|
-
export
|
|
4
|
+
export interface ContextExtension {
|
|
5
|
+
}
|
|
6
|
+
export declare class ContextService implements ContextExtension {
|
|
5
7
|
static readonly name = "ContextService";
|
|
6
8
|
private readonly storage;
|
|
7
9
|
init(): onRequestHookHandler;
|
|
10
|
+
isInitialized(): boolean;
|
|
8
11
|
get<T>(key: Key, throwOnMissing: true): T;
|
|
9
12
|
get<T>(key: Key, throwOnMissing?: boolean): T | null;
|
|
10
13
|
getFromParent<T>(key: Key, throwOnMissing: true): T;
|
|
@@ -20,5 +23,6 @@ export declare class ContextService {
|
|
|
20
23
|
getResponse(throwOnMissing: false): HttpResponse | null;
|
|
21
24
|
getRID(): string;
|
|
22
25
|
getRID(throwOnMissing: false): string | null;
|
|
26
|
+
extend<T extends ContextExtension = ContextExtension>(extension: T & ThisType<this & T>): this;
|
|
23
27
|
}
|
|
24
28
|
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)
|
|
@@ -89,6 +92,9 @@ let ContextService = class ContextService {
|
|
|
89
92
|
getRID(throwOnMissing = true) {
|
|
90
93
|
return this.get(RID, throwOnMissing);
|
|
91
94
|
}
|
|
95
|
+
extend(extension) {
|
|
96
|
+
return Object.assign(this, extension);
|
|
97
|
+
}
|
|
92
98
|
};
|
|
93
99
|
exports.ContextService = ContextService;
|
|
94
100
|
exports.ContextService = ContextService = __decorate([
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { onRequestHookHandler } from 'fastify';
|
|
2
2
|
import { HttpRequest, HttpResponse } from '../interfaces/index.js';
|
|
3
3
|
type Key = string | symbol;
|
|
4
|
-
export
|
|
4
|
+
export interface ContextExtension {
|
|
5
|
+
}
|
|
6
|
+
export declare class ContextService implements ContextExtension {
|
|
5
7
|
static readonly name = "ContextService";
|
|
6
8
|
private readonly storage;
|
|
7
9
|
init(): onRequestHookHandler;
|
|
10
|
+
isInitialized(): boolean;
|
|
8
11
|
get<T>(key: Key, throwOnMissing: true): T;
|
|
9
12
|
get<T>(key: Key, throwOnMissing?: boolean): T | null;
|
|
10
13
|
getFromParent<T>(key: Key, throwOnMissing: true): T;
|
|
@@ -20,5 +23,6 @@ export declare class ContextService {
|
|
|
20
23
|
getResponse(throwOnMissing: false): HttpResponse | null;
|
|
21
24
|
getRID(): string;
|
|
22
25
|
getRID(throwOnMissing: false): string | null;
|
|
26
|
+
extend<T extends ContextExtension = ContextExtension>(extension: T & ThisType<this & T>): this;
|
|
23
27
|
}
|
|
24
28
|
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)
|
|
@@ -86,6 +89,9 @@ let ContextService = class ContextService {
|
|
|
86
89
|
getRID(throwOnMissing = true) {
|
|
87
90
|
return this.get(RID, throwOnMissing);
|
|
88
91
|
}
|
|
92
|
+
extend(extension) {
|
|
93
|
+
return Object.assign(this, extension);
|
|
94
|
+
}
|
|
89
95
|
};
|
|
90
96
|
ContextService = __decorate([
|
|
91
97
|
Injectable()
|
package/package.json
CHANGED